Skip to content

Commit e00bc6c

Browse files
committed
Fix block rejection: witness tx relay, MWEB state, mempool validation
Three fixes for blocks rejected by litecoind: 1. Request MSG_WITNESS_TX (not MSG_TX) for P2P inv announcements so segwit transactions arrive with witness data — fixes CheckQueue script verification failures when mempool txs are included in blocks. 2. MWEBTracker rejects blocks older than current state, preventing bootstrap's 288 historical blocks from overwriting the tip's HogEx reference with stale outpoints — fixes bad-txns-inputs-missingorspent on blocks with only coinbase + HogEx. 3. Mempool UTXO guard at template selection time + revalidate_inputs() after block connection evicts stale txs whose inputs were spent by confirmed blocks — prevents inclusion of double-spend transactions. Also: BIP 152 v2 compact blocks now use wtxid for short IDs, and getblocktxn handler serves missing txs from cached sent blocks. Tested on LTC testnet: 5 consecutive blocks accepted (including blocks with 1 and 3 mempool transactions), zero validation failures.
1 parent 2fe808a commit e00bc6c

5 files changed

Lines changed: 149 additions & 18 deletions

File tree

src/c2pool/c2pool_refactored.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,7 @@ int main(int argc, char* argv[]) {
13831383
ltc_utxo_db = std::make_unique<core::coin::UTXOViewDB>(utxo_path, utxo_opts);
13841384
if (ltc_utxo_db->open()) {
13851385
ltc_utxo_cache = std::make_unique<core::coin::UTXOViewCache>(ltc_utxo_db.get());
1386+
embedded_pool->set_utxo(ltc_utxo_cache.get());
13861387
LOG_INFO << "[EMB-LTC] UTXO set opened: best_height=" << ltc_utxo_db->get_best_height()
13871388
<< " best_block=" << ltc_utxo_db->get_best_block().GetHex().substr(0, 16);
13881389
} else {
@@ -1815,9 +1816,15 @@ int main(int argc, char* argv[]) {
18151816
// may now be in the UTXO set (especially bootstrap blocks).
18161817
if (utxo) {
18171818
int resolved = pool->recompute_unknown_fees(utxo);
1818-
if (resolved > 0) {
1819-
// Fees changed — trigger template rebuild so
1820-
// coinbasevalue includes the newly-resolved fees.
1819+
// Evict fee-known txs whose inputs are now spent.
1820+
// remove_for_block() only catches direct conflicts
1821+
// via m_spent_outputs; this catches any remaining
1822+
// stale txs (e.g., inputs spent by a tx the mempool
1823+
// never tracked).
1824+
int evicted = pool->revalidate_inputs(utxo);
1825+
if (resolved > 0 || evicted > 0) {
1826+
// Fees changed or stale txs evicted — trigger
1827+
// template rebuild so miners get clean work.
18211828
web_server.trigger_work_refresh_debounced();
18221829
}
18231830
}
@@ -3605,6 +3612,7 @@ int main(int argc, char* argv[]) {
36053612
doge_utxo_db = std::make_unique<core::coin::UTXOViewDB>(utxo_path, utxo_opts);
36063613
if (doge_utxo_db->open()) {
36073614
doge_utxo_cache = std::make_unique<core::coin::UTXOViewCache>(doge_utxo_db.get());
3615+
if (doge_pool) doge_pool->set_utxo(doge_utxo_cache.get());
36083616
LOG_INFO << "[EMB-DOGE] UTXO set opened: best_height=" << doge_utxo_db->get_best_height();
36093617
} else {
36103618
LOG_WARNING << "[EMB-DOGE] UTXO DB failed to open — fees will be unknown";
@@ -3903,9 +3911,11 @@ int main(int argc, char* argv[]) {
39033911
}
39043912
if (utxo) {
39053913
int resolved = pool->recompute_unknown_fees(utxo);
3906-
if (resolved > 0) {
3914+
int evicted = pool->revalidate_inputs(utxo);
3915+
if (resolved > 0 || evicted > 0) {
39073916
LOG_INFO << "[EMB-DOGE] Fee revalidation: resolved=" << resolved
3908-
<< " pool_size=" << after;
3917+
<< " evicted=" << evicted
3918+
<< " pool_size=" << pool->size();
39093919
web_server.trigger_work_refresh_debounced();
39103920
}
39113921
}

src/impl/ltc/coin/compact_blocks.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,14 @@ inline CompactBlock BuildCompactBlock(const BlockHeaderType& header,
218218
}
219219

220220
// Remaining txs as short IDs (pre-compute SipHash keys once)
221+
// BIP 152 v2: use wtxid (witness serialization hash) for short IDs.
221222
if (txs.size() > 1) {
222223
uint64_t k0, k1;
223224
cb.GetSipHashKeys(k0, k1);
224225
for (size_t i = 1; i < txs.size(); ++i) {
225-
uint256 txid = compute_txid(txs[i]);
226-
cb.short_ids.push_back(CompactBlock::GetShortID(k0, k1, txid));
226+
auto packed = pack(TX_WITH_WITNESS(txs[i]));
227+
uint256 wtxid = Hash(packed.get_span());
228+
cb.short_ids.push_back(CompactBlock::GetShortID(k0, k1, wtxid));
227229
}
228230
}
229231

@@ -241,7 +243,7 @@ struct CompactBlockReconstructionResult {
241243

242244
/// Attempt to reconstruct a full block from a compact block + known transactions.
243245
/// @param cb The received compact block.
244-
/// @param known_txs Map of txid → transaction (typically from mempool).
246+
/// @param known_txs Map of wtxid → transaction (BIP 152 v2 uses wtxid for short ID matching).
245247
/// @return Result with reconstructed block (if complete) or missing indexes.
246248
inline CompactBlockReconstructionResult ReconstructBlock(
247249
const CompactBlock& cb,

src/impl/ltc/coin/mempool.hpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <core/log.hpp>
1919
#include <core/coin/utxo_view_cache.hpp>
2020

21+
#include <atomic>
2122
#include <map>
2223
#include <mutex>
2324
#include <set>
@@ -91,6 +92,7 @@ class Mempool {
9192
/// Update the current chain tip height (for coinbase maturity checks).
9293
/// Call after each block connection.
9394
void set_tip_height(uint32_t h) { m_tip_height = h; }
95+
void set_utxo(core::coin::UTXOViewCache* u) { m_utxo.store(u); }
9496

9597
// Disable copy
9698
Mempool(const Mempool&) = delete;
@@ -350,13 +352,33 @@ class Mempool {
350352
std::set<uint256> selected;
351353

352354
// Pass 1: highest feerate first (known-fee txs)
355+
auto* utxo = m_utxo.load();
353356
for (auto it = m_feerate_index.begin(); it != m_feerate_index.end(); ++it) {
354357
auto pit = m_pool.find(it->second);
355358
if (pit == m_pool.end()) continue;
356359
const auto& entry = pit->second;
357360
if (!entry.fee_known) continue;
358361
if (total_weight + entry.weight > max_weight) continue;
359362

363+
// Guard: verify inputs still exist in UTXO (or parent mempool tx).
364+
// Catches stale txs in the window between tip-change and full_block arrival.
365+
if (utxo) {
366+
bool inputs_ok = true;
367+
for (const auto& vin : entry.tx.vin) {
368+
core::coin::Outpoint op(vin.prevout.hash, vin.prevout.index);
369+
core::coin::Coin coin;
370+
if (!utxo->get_coin(op, coin)) {
371+
// Check if parent is in mempool (CPFP chain)
372+
if (!m_pool.count(vin.prevout.hash) ||
373+
vin.prevout.index >= m_pool.at(vin.prevout.hash).tx.vout.size()) {
374+
inputs_ok = false;
375+
break;
376+
}
377+
}
378+
}
379+
if (!inputs_ok) continue; // skip stale tx
380+
}
381+
360382
total_weight += entry.weight;
361383
total_fees += entry.fee;
362384
result.push_back({entry.tx, entry.fee, true});
@@ -399,6 +421,43 @@ class Mempool {
399421
return resolved;
400422
}
401423

424+
/// Re-validate all fee-known mempool transactions against the UTXO set.
425+
/// Evicts any transaction whose inputs are no longer in the UTXO set
426+
/// (i.e., were spent by a confirmed block but not caught by remove_for_block's
427+
/// conflict detection — e.g., when the spending tx wasn't tracked in m_spent_outputs).
428+
/// Call after remove_for_block() + UTXO connect to catch stale transactions.
429+
/// Returns the number of evicted transactions.
430+
int revalidate_inputs(core::coin::UTXOViewCache* utxo) {
431+
if (!utxo) return 0;
432+
std::lock_guard<std::mutex> lock(m_mutex);
433+
434+
std::vector<uint256> to_remove;
435+
for (const auto& [txid, entry] : m_pool) {
436+
if (!entry.fee_known) continue; // already quarantined
437+
for (const auto& vin : entry.tx.vin) {
438+
core::coin::Outpoint op(vin.prevout.hash, vin.prevout.index);
439+
core::coin::Coin coin;
440+
if (!utxo->get_coin(op, coin)) {
441+
// Input not in UTXO — check if parent is in mempool (CPFP)
442+
if (m_pool.count(vin.prevout.hash) &&
443+
vin.prevout.index < m_pool.at(vin.prevout.hash).tx.vout.size())
444+
continue; // parent still in mempool, tx is valid
445+
to_remove.push_back(txid);
446+
break;
447+
}
448+
}
449+
}
450+
451+
for (const auto& txid : to_remove)
452+
remove_tx_locked(txid);
453+
454+
if (!to_remove.empty()) {
455+
LOG_INFO << "[EMB] Mempool revalidate: evicted " << to_remove.size()
456+
<< " stale txs (inputs spent), remaining=" << m_pool.size();
457+
}
458+
return static_cast<int>(to_remove.size());
459+
}
460+
402461
/// Snapshot of all txids currently in the pool.
403462
std::vector<uint256> all_txids() const {
404463
std::lock_guard<std::mutex> lock(m_mutex);
@@ -418,6 +477,18 @@ class Mempool {
418477
return out;
419478
}
420479

480+
/// Return all transactions keyed by wtxid (BIP 152 v2 compact block reconstruction).
481+
std::map<uint256, MutableTransaction> all_txs_map_wtxid() const {
482+
std::lock_guard<std::mutex> lock(m_mutex);
483+
std::map<uint256, MutableTransaction> out;
484+
for (const auto& [txid, entry] : m_pool) {
485+
auto packed = pack(TX_WITH_WITNESS(entry.tx));
486+
uint256 wtxid = Hash(packed.get_span());
487+
out[wtxid] = entry.tx;
488+
}
489+
return out;
490+
}
491+
421492
private:
422493
// ─── Internal (caller holds mutex) ───────────────────────────────────
423494

@@ -568,6 +639,7 @@ class Mempool {
568639
time_t m_expiry_sec;
569640
core::coin::ChainLimits m_limits; // MoneyRange + maturity constants
570641
uint32_t m_tip_height{0}; // current chain tip (for maturity checks)
642+
std::atomic<core::coin::UTXOViewCache*> m_utxo{nullptr}; // for template-time input validation
571643
};
572644

573645
} // namespace coin

src/impl/ltc/coin/mweb_builder.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ class MWEBTracker {
388388
public:
389389
/// Update state from a full block.
390390
/// Called when a new block is received via P2P.
391+
/// Only accepts blocks that advance the state — bootstrap/historical blocks
392+
/// are silently ignored to prevent stale HogEx references.
391393
bool update(const BlockType& block, uint32_t height,
392394
const std::vector<unsigned char>& mweb_raw) {
393395
if (mweb_raw.empty()) {
@@ -400,6 +402,12 @@ class MWEBTracker {
400402
if (!MWEBBuilder::extract_mweb_header_from_raw(mweb_raw, new_state))
401403
return false;
402404
std::lock_guard<std::mutex> lock(m_mutex);
405+
// Only accept blocks that advance or match current height.
406+
// Bootstrap delivers hundreds of old blocks whose HogEx references
407+
// are long spent — accepting them would produce invalid templates.
408+
if (m_state.valid && height < m_state.captured_at_height) {
409+
return false;
410+
}
403411
m_state = std::move(new_state);
404412
return true;
405413
}

src/impl/ltc/coin/p2p_node.hpp

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
7777
// Compact block reconstruction state: pending compact block awaiting blocktxn
7878
std::unique_ptr<CompactBlock> m_pending_cmpct;
7979
std::vector<uint32_t> m_pending_missing_indexes;
80+
// Last compact block we SENT — cached to serve getblocktxn requests
81+
BlockType m_sent_cmpct_block;
82+
uint256 m_sent_cmpct_hash;
8083
// External mempool for compact block tx matching
8184
Mempool* m_mempool{nullptr};
8285

@@ -314,7 +317,8 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
314317
void enable_mempool_request() { m_request_mempool_on_connect = true; }
315318

316319
/// Relay a pre-serialized block via P2P.
317-
/// Automatically uses compact block format for peers that support it.
320+
/// Uses compact block format (BIP 152 v2) for peers that support it,
321+
/// falling back to full block otherwise.
318322
void submit_block_raw(const std::vector<unsigned char>& block_bytes)
319323
{
320324
if (!m_peer) return;
@@ -332,6 +336,11 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
332336

333337
auto packed_hdr = pack(static_cast<BlockHeaderType&>(block));
334338
auto blockhash = Hash(packed_hdr.get_span());
339+
340+
// Cache the full block so we can serve getblocktxn requests.
341+
m_sent_cmpct_block = std::move(block);
342+
m_sent_cmpct_hash = blockhash;
343+
335344
LOG_INFO << "[" << m_chain_label << "] Sent compact block "
336345
<< blockhash.GetHex()
337346
<< " (" << cb.short_ids.size() << " short IDs, "
@@ -535,7 +544,11 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
535544
switch (btype)
536545
{
537546
case inventory_type::tx:
538-
vinv.push_back(inv);
547+
// Always request with witness (MSG_WITNESS_TX) so segwit
548+
// transactions arrive with their witness data intact.
549+
// Without this, P2WPKH/P2WSH spends arrive stripped and
550+
// fail CheckQueue when included in blocks.
551+
vinv.push_back(inventory_type(inventory_type::witness_tx, inv.m_hash));
539552
break;
540553
case inventory_type::block:
541554
m_coin->new_block.happened(inv.m_hash);
@@ -733,16 +746,20 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
733746
m_coin->new_block.happened(blockhash);
734747

735748
// Attempt reconstruction from mempool + known_txs
749+
// BIP 152 v2: short IDs are keyed by wtxid (witness txid).
736750
std::map<uint256, MutableTransaction> known;
737751

738-
// Gather from node's known_txs
752+
// Gather from node's known_txs (re-key by wtxid)
739753
for (const auto& [txid, tx] : m_coin->known_txs) {
740-
known[txid] = MutableTransaction(tx);
754+
MutableTransaction mtx(tx);
755+
auto packed = pack(TX_WITH_WITNESS(mtx));
756+
uint256 wtxid = Hash(packed.get_span());
757+
known[wtxid] = std::move(mtx);
741758
}
742759

743-
// Gather from mempool
760+
// Gather from mempool (wtxid-keyed)
744761
if (m_mempool) {
745-
auto mp_txs = m_mempool->all_txs_map();
762+
auto mp_txs = m_mempool->all_txs_map_wtxid();
746763
known.merge(mp_txs);
747764
}
748765

@@ -776,10 +793,32 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
776793

777794
ADD_P2P_HANDLER(getblocktxn)
778795
{
779-
// BIP 152: Peer requests missing transactions — we don't serve blocks
780-
LOG_DEBUG_COIND << "[" << m_chain_label << "] Peer requests "
781-
<< msg->m_request.indexes.size() << " txs via getblocktxn for "
782-
<< msg->m_request.blockhash.GetHex() << " (ignored, we don't serve blocks)";
796+
auto& req = msg->m_request;
797+
798+
// Only serve our most recently sent compact block
799+
if (req.blockhash != m_sent_cmpct_hash || m_sent_cmpct_block.m_txs.empty()) {
800+
LOG_DEBUG_COIND << "[" << m_chain_label << "] getblocktxn for unknown block "
801+
<< req.blockhash.GetHex() << " — ignoring";
802+
return;
803+
}
804+
805+
BlockTransactionsResponse resp;
806+
resp.blockhash = req.blockhash;
807+
resp.txs.reserve(req.indexes.size());
808+
809+
for (uint32_t idx : req.indexes) {
810+
if (idx >= m_sent_cmpct_block.m_txs.size()) {
811+
LOG_WARNING << "[" << m_chain_label << "] getblocktxn: index " << idx
812+
<< " out of range (block has " << m_sent_cmpct_block.m_txs.size() << " txs)";
813+
return; // malformed request — drop
814+
}
815+
resp.txs.push_back(m_sent_cmpct_block.m_txs[idx]);
816+
}
817+
818+
auto rmsg = message_blocktxn::make_raw(resp);
819+
m_peer->write(rmsg);
820+
LOG_INFO << "[" << m_chain_label << "] Served " << resp.txs.size()
821+
<< " txs via blocktxn for " << req.blockhash.GetHex();
783822
}
784823

785824
ADD_P2P_HANDLER(blocktxn)

0 commit comments

Comments
 (0)