Skip to content

Commit 57884f0

Browse files
Merge #6825: feat: add special transaction support to compact block filters (BIP-157/158)
4b90441 docs: indicate that blockfilter will be re-created (PastaPastaPasta) b2522a2 chore: make clang format happy (UdjinM6) 09490b7 chore: drop useless feature_blockfilter_version.py (UdjinM6) 4750246 doc: shrink release notes (UdjinM6) f28bf6d feat: start blockfilter sync from scratch on db version upgrades (UdjinM6) d3f1b9e test: remove wallet dependency check from blockfilter tests (pasta) 492c109 feat: add versioning to blockfilter index to detect incompatible format (pasta) bf352e7 evo: use Span<const unsigned char> in specialtx filter extraction callback to avoid temporary allocations; adjust helpers and call site (pasta) c734b64 tests: p2p_blockfilters: use assert_greater_than for clearer failures (pasta) a5bab55 test: remove wallet dependency from p2p_blockfilters.py and refactor duplicated code (Konstantin Akimov) a1ecf5d tests: p2p_blockfilters: skip when wallet module is not available (pasta) 9266b9a refactor: break circular dependency over evo/assetlock and llmq/signing (Konstantin Akimov) 1c9809f filters: add cross-reference comments to keep bloom and compact filter extraction in sync (pasta) 7a33c14 tests: compact filters: fix F841, verify tx inclusion, and cleanups (pasta) 3fb9183 test: improve p2p_blockfilters special transaction tests (pasta) 8d08a54 Apply suggestions from code review (PastaPastaPasta) 0ea85f7 fix: remove unused variable in blockfilter_tests (pasta) 773fe9b lint: add specialtx_filter circular dependencies to allowed list (pasta) 8ae2280 feat: add special transaction support to compact block filters (BIP-157/158) (pasta) Pull request description: ## Summary - Implements extraction of special transaction fields for compact block filters - Achieves feature parity between bloom filters and compact block filters for special transactions - Enables SPV clients to detect special transactions using either filtering mechanism ## What this PR does This PR adds support for Dash special transactions to the compact block filter implementation (BIP-157/158). Previously, only bloom filters could detect special transaction fields, leaving compact block filter users unable to track masternode-related transactions and platform operations. ### Implementation Details The implementation uses a delegation pattern to extract special transaction fields without creating circular dependencies: - `ExtractSpecialTxFilterElements()` function extracts relevant fields from each special transaction type - Fields extracted match exactly those in the bloom filter implementation (`CheckSpecialTransactionMatchesAndUpdate`) - All 5 special transaction types are supported: - **ProRegTx**: collateral outpoint, owner/voting keys, payout script - **ProUpServTx**: ProTx hash, operator payout script - **ProUpRegTx**: ProTx hash, voting key, payout script - **ProUpRevTx**: ProTx hash - **AssetLockTx**: credit output scripts (excluding OP_RETURN) ### Testing Comprehensive test coverage includes: - **Unit tests**: Test all 5 special transaction types with realistic payloads - **Functional tests**: Create actual AssetLockTx transactions with proper signatures - **Edge cases**: Empty scripts, OP_RETURN exclusion, multiple outputs ## Test plan - [x] Unit tests pass (`make check`) - [x] Functional test passes (`test/functional/p2p_blockfilters.py`) - [x] All special transaction types covered in tests - [x] Verified special fields are added to filters (via debug logging) 🤖 Generated with [Claude Code](https://claude.ai/code) ACKs for top commit: knst: utACK 4b90441 UdjinM6: utACK 4b90441 Tree-SHA512: 673b47ef8626761cdffc659c6ca0ffd214b6d3e7ea6086976528ec79967f5a6a6d864b63cc1e5bac38f079fa092c49637f334c058231b4f6c687f9dae585db27
2 parents 445300c + 4b90441 commit 57884f0

12 files changed

+679
-6
lines changed

doc/release-notes-6825.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Breaking Change: Block Filter Index Format Update
2+
3+
## Summary
4+
The compact block filter index format has been updated to include Dash special transaction data, providing feature parity with bloom filters for SPV client support. This change is **incompatible** with existing blockfilter indexes. Existing blockfilter indexes will automatically be re-created with the new version.
5+
6+
## Technical Details
7+
- The blockfilter index now includes fields from Dash special transactions:
8+
- ProRegTx (masternode registration)
9+
- ProUpServTx (masternode service updates)
10+
- ProUpRegTx (masternode operator updates)
11+
- ProUpRevTx (masternode revocation)
12+
- AssetLockTx (platform credit outputs)
13+
- A versioning system has been added to detect incompatible indexes on startup
14+
- The index version is now 2 (previously unversioned)
15+
16+
## Benefits
17+
- SPV clients can now detect and track Dash-specific transactions
18+
- Feature parity between bloom filters and compact block filters
19+
- Protection against serving incorrect filter data to light clients

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ BITCOIN_CORE_H = \
202202
evo/simplifiedmns.h \
203203
evo/smldiff.h \
204204
evo/specialtx.h \
205+
evo/specialtx_filter.h \
205206
evo/specialtxman.h \
206207
dsnotificationinterface.h \
207208
governance/governance.h \
@@ -481,6 +482,7 @@ libbitcoin_node_a_SOURCES = \
481482
evo/simplifiedmns.cpp \
482483
evo/smldiff.cpp \
483484
evo/specialtx.cpp \
485+
evo/specialtx_filter.cpp \
484486
evo/specialtxman.cpp \
485487
flatfile.cpp \
486488
governance/classes.cpp \

src/blockfilter.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <blockfilter.h>
99
#include <crypto/siphash.h>
10+
#include <evo/specialtx_filter.h>
1011
#include <hash.h>
1112
#include <primitives/transaction.h>
1213
#include <script/script.h>
@@ -195,6 +196,11 @@ static GCSFilter::ElementSet BasicFilterElements(const CBlock& block,
195196
if (script.empty() || script[0] == OP_RETURN) continue;
196197
elements.emplace(script.begin(), script.end());
197198
}
199+
200+
// Extract special transaction elements using delegation pattern
201+
ExtractSpecialTxFilterElements(*tx, [&elements](Span<const unsigned char> data) {
202+
elements.emplace(data.begin(), data.end());
203+
});
198204
}
199205

200206
for (const CTxUndo& tx_undo : block_undo.vtxundo) {

src/common/bloom.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ bool CBloomFilter::CheckScript(const CScript &script) const
119119
// Filter is updated only if it has BLOOM_UPDATE_ALL flag to be able to have
120120
// simple SPV wallets that doesn't work with DIP2 transactions (multicoin
121121
// wallets, etc.)
122+
// NOTE(maintenance): Keep this implementation in sync with
123+
// ExtractSpecialTxFilterElements in src/evo/specialtx_filter.cpp.
124+
// Both routines must handle the same set of special-transaction fields.
125+
// If you modify one, update the other to prevent mismatches between
126+
// bloom filter relevance and compact filter element extraction.
122127
bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &tx)
123128
{
124129
if (!tx.HasExtraPayloadField()) {

src/evo/assetlocktx.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
#include <evo/specialtx.h>
77

88
#include <llmq/commitment.h>
9-
#include <llmq/signing.h>
109
#include <llmq/quorums.h>
1110

1211
#include <chainparams.h>
@@ -22,6 +21,11 @@
2221

2322
using node::BlockManager;
2423

24+
namespace llmq {
25+
// forward declaration to avoid circular dependency
26+
uint256 BuildSignHash(Consensus::LLMQType llmqType, const uint256& quorumHash, const uint256& id, const uint256& msgHash);
27+
} // namespace llmq
28+
2529
/**
2630
* Common code for Asset Lock and Asset Unlock
2731
*/

src/evo/specialtx_filter.cpp

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) 2025 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <evo/specialtx_filter.h>
6+
7+
#include <evo/assetlocktx.h>
8+
#include <evo/providertx.h>
9+
#include <evo/specialtx.h>
10+
#include <primitives/transaction.h>
11+
#include <script/script.h>
12+
#include <span.h>
13+
#include <streams.h>
14+
15+
/**
16+
* Rationale for Special Transaction Field Extraction:
17+
*
18+
* This implementation extracts specific fields from Dash special transactions
19+
* to maintain parity with the bloom filter implementation (CBloomFilter::CheckSpecialTransactionMatchesAndUpdate).
20+
*
21+
* The fields extracted are those that SPV clients might need to detect:
22+
* - Owner/Voting keys: To track masternode ownership and voting rights
23+
* - Payout scripts: To detect payments to specific addresses
24+
* - ProTx hashes: To track masternode lifecycle and updates
25+
* - Collateral outpoints: To track masternode collateral
26+
* - Credit outputs: To track platform-related transactions
27+
*
28+
* Each transaction type has different fields based on its purpose:
29+
* - ProRegTx: All identity and payout fields (initial registration)
30+
* - ProUpServTx: ProTx hash and operator payout (service updates)
31+
* - ProUpRegTx: ProTx hash, voting key, and payout (ownership updates)
32+
* - ProUpRevTx: ProTx hash only (revocation tracking)
33+
* - AssetLockTx: Credit output scripts (platform credits)
34+
*/
35+
// Helper function to add a script to the filter if it's not empty
36+
static void AddScriptElement(const CScript& script, const std::function<void(Span<const unsigned char>)>& addElement)
37+
{
38+
if (!script.empty()) {
39+
addElement(MakeUCharSpan(script));
40+
}
41+
}
42+
43+
// Helper function to add a hash/key to the filter
44+
template <typename T>
45+
static void AddHashElement(const T& hash, const std::function<void(Span<const unsigned char>)>& addElement)
46+
{
47+
addElement(MakeUCharSpan(hash));
48+
}
49+
50+
// NOTE(maintenance): Keep this in sync with
51+
// CBloomFilter::CheckSpecialTransactionMatchesAndUpdate in
52+
// src/common/bloom.cpp. If you add or remove fields for a special
53+
// transaction type here, update the bloom filter routine accordingly
54+
// (and vice versa) to avoid compact-filter vs bloom-filter divergence.
55+
void ExtractSpecialTxFilterElements(const CTransaction& tx, const std::function<void(Span<const unsigned char>)>& addElement)
56+
{
57+
if (!tx.HasExtraPayloadField()) {
58+
return; // not a special transaction
59+
}
60+
61+
switch (tx.nType) {
62+
case TRANSACTION_PROVIDER_REGISTER: {
63+
if (const auto opt_proTx = GetTxPayload<CProRegTx>(tx)) {
64+
// Add collateral outpoint
65+
CDataStream stream(SER_NETWORK, PROTOCOL_VERSION);
66+
stream << opt_proTx->collateralOutpoint;
67+
addElement(MakeUCharSpan(stream));
68+
69+
// Add owner key ID
70+
AddHashElement(opt_proTx->keyIDOwner, addElement);
71+
72+
// Add voting key ID
73+
AddHashElement(opt_proTx->keyIDVoting, addElement);
74+
75+
// Add payout script
76+
AddScriptElement(opt_proTx->scriptPayout, addElement);
77+
}
78+
break;
79+
}
80+
case TRANSACTION_PROVIDER_UPDATE_SERVICE: {
81+
if (const auto opt_proTx = GetTxPayload<CProUpServTx>(tx)) {
82+
// Add ProTx hash
83+
AddHashElement(opt_proTx->proTxHash, addElement);
84+
85+
// Add operator payout script
86+
AddScriptElement(opt_proTx->scriptOperatorPayout, addElement);
87+
}
88+
break;
89+
}
90+
case TRANSACTION_PROVIDER_UPDATE_REGISTRAR: {
91+
if (const auto opt_proTx = GetTxPayload<CProUpRegTx>(tx)) {
92+
// Add ProTx hash
93+
AddHashElement(opt_proTx->proTxHash, addElement);
94+
95+
// Add voting key ID
96+
AddHashElement(opt_proTx->keyIDVoting, addElement);
97+
98+
// Add payout script
99+
AddScriptElement(opt_proTx->scriptPayout, addElement);
100+
}
101+
break;
102+
}
103+
case TRANSACTION_PROVIDER_UPDATE_REVOKE: {
104+
if (const auto opt_proTx = GetTxPayload<CProUpRevTx>(tx)) {
105+
// Add ProTx hash
106+
AddHashElement(opt_proTx->proTxHash, addElement);
107+
}
108+
break;
109+
}
110+
case TRANSACTION_ASSET_LOCK: {
111+
// Asset Lock transactions have special outputs (creditOutputs) that should be included
112+
if (const auto opt_assetlockTx = GetTxPayload<CAssetLockPayload>(tx)) {
113+
const auto& extraOuts = opt_assetlockTx->getCreditOutputs();
114+
for (const CTxOut& txout : extraOuts) {
115+
const CScript& script = txout.scriptPubKey;
116+
// Exclude OP_RETURN outputs as they are not spendable
117+
if (!script.empty() && script[0] != OP_RETURN) {
118+
AddScriptElement(script, addElement);
119+
}
120+
}
121+
}
122+
break;
123+
}
124+
case TRANSACTION_ASSET_UNLOCK:
125+
case TRANSACTION_COINBASE:
126+
case TRANSACTION_QUORUM_COMMITMENT:
127+
case TRANSACTION_MNHF_SIGNAL:
128+
// No additional special fields needed for these transaction types
129+
// Their standard outputs are already included in the base filter
130+
break;
131+
} // no default case, so the compiler can warn about missing cases
132+
}

src/evo/specialtx_filter.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2025 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_EVO_SPECIALTX_FILTER_H
6+
#define BITCOIN_EVO_SPECIALTX_FILTER_H
7+
8+
#include <functional>
9+
#include <span.h>
10+
11+
class CTransaction;
12+
13+
/**
14+
* Extract filterable elements from special transactions for use in compact block filters.
15+
* This function extracts the same fields that are included in bloom filters to ensure
16+
* SPV clients can detect special transactions using either filtering mechanism.
17+
*
18+
* @param tx The transaction to extract elements from
19+
* @param addElement Callback to add extracted elements to the filter. Uses
20+
* Span<const unsigned char> to avoid intermediate
21+
* allocations.
22+
*/
23+
void ExtractSpecialTxFilterElements(const CTransaction& tx,
24+
const std::function<void(Span<const unsigned char>)>& addElement);
25+
26+
#endif // BITCOIN_EVO_SPECIALTX_FILTER_H

src/index/blockfilterindex.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ using node::UndoReadFromDisk;
3030
constexpr uint8_t DB_BLOCK_HASH{'s'};
3131
constexpr uint8_t DB_BLOCK_HEIGHT{'t'};
3232
constexpr uint8_t DB_FILTER_POS{'P'};
33+
constexpr uint8_t DB_VERSION{'V'};
3334

3435
constexpr unsigned int MAX_FLTR_FILE_SIZE = 0x1000000; // 16 MiB
3536
/** The pre-allocation chunk size for fltr?????.dat files */
@@ -111,11 +112,34 @@ BlockFilterIndex::BlockFilterIndex(BlockFilterType filter_type,
111112

112113
m_name = filter_name + " block filter index";
113114
m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe);
115+
116+
// Check version
117+
int version = 0;
118+
if (!m_db->Read(DB_VERSION, version) || version < CURRENT_VERSION) {
119+
// No version or too old version means we need to start from scratch
120+
LogPrintf("%s: Outdated or no version blockfilter, starting from scratch\n", __func__);
121+
m_db.reset();
122+
m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, /*f_wipe=*/true);
123+
m_db->Write(DB_VERSION, CURRENT_VERSION);
124+
}
125+
114126
m_filter_fileseq = std::make_unique<FlatFileSeq>(std::move(path), "fltr", FLTR_FILE_CHUNK_SIZE);
115127
}
116128

117129
bool BlockFilterIndex::Init()
118130
{
131+
// Check version compatibility first
132+
int version = 0;
133+
if (m_db->Exists(DB_VERSION)) {
134+
if (!m_db->Read(DB_VERSION, version)) {
135+
return error("%s: Failed to read %s index version from database", __func__, GetName());
136+
}
137+
if (version > CURRENT_VERSION) {
138+
return error("%s: %s index version %d is too high (expected <= %d)",
139+
__func__, GetName(), version, CURRENT_VERSION);
140+
}
141+
}
142+
119143
if (!m_db->Read(DB_FILTER_POS, m_next_filter_pos)) {
120144
// Check that the cause of the read failure is that the key does not exist. Any other errors
121145
// indicate database corruption or a disk failure, and starting the index would cause
@@ -136,6 +160,11 @@ bool BlockFilterIndex::CommitInternal(CDBBatch& batch)
136160
{
137161
const FlatFilePos& pos = m_next_filter_pos;
138162

163+
// Write the current version if this is a new index
164+
if (!m_db->Exists(DB_VERSION)) {
165+
batch.Write(DB_VERSION, CURRENT_VERSION);
166+
}
167+
139168
// Flush current filter file to disk.
140169
CAutoFile file(m_filter_fileseq->Open(pos), SER_DISK, CLIENT_VERSION);
141170
if (file.IsNull()) {

src/index/blockfilterindex.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ static constexpr int CFCHECKPT_INTERVAL = 1000;
2525
class BlockFilterIndex final : public BaseIndex
2626
{
2727
private:
28+
/** Version of the blockfilter index format. Increment this when breaking changes are made. */
29+
static constexpr int CURRENT_VERSION = 2;
30+
2831
BlockFilterType m_filter_type;
2932
std::string m_name;
3033
std::unique_ptr<BaseIndex::DB> m_db;

0 commit comments

Comments
 (0)