-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: add special transaction support to compact block filters (BIP-157/158) #6825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8ae2280
773fe9b
0ea85f7
8d08a54
3fb9183
7a33c14
1c9809f
9266b9a
a1ecf5d
a5bab55
c734b64
bf352e7
492c109
d3f1b9e
f28bf6d
4750246
09490b7
b2522a2
4b90441
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Breaking Change: Block Filter Index Format Update | ||
|
||
## Summary | ||
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. | ||
|
||
## Technical Details | ||
- The blockfilter index now includes fields from Dash special transactions: | ||
- ProRegTx (masternode registration) | ||
- ProUpServTx (masternode service updates) | ||
- ProUpRegTx (masternode operator updates) | ||
- ProUpRevTx (masternode revocation) | ||
- AssetLockTx (platform credit outputs) | ||
- A versioning system has been added to detect incompatible indexes on startup | ||
- The index version is now 2 (previously unversioned) | ||
|
||
## Benefits | ||
- SPV clients can now detect and track Dash-specific transactions | ||
- Feature parity between bloom filters and compact block filters | ||
- Protection against serving incorrect filter data to light clients |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
// Copyright (c) 2025 The Dash Core developers | ||
// Distributed under the MIT software license, see the accompanying | ||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
||
#include <evo/specialtx_filter.h> | ||
|
||
#include <evo/assetlocktx.h> | ||
#include <evo/providertx.h> | ||
#include <evo/specialtx.h> | ||
#include <primitives/transaction.h> | ||
#include <script/script.h> | ||
#include <span.h> | ||
#include <streams.h> | ||
|
||
/** | ||
* Rationale for Special Transaction Field Extraction: | ||
* | ||
* This implementation extracts specific fields from Dash special transactions | ||
* to maintain parity with the bloom filter implementation (CBloomFilter::CheckSpecialTransactionMatchesAndUpdate). | ||
* | ||
* The fields extracted are those that SPV clients might need to detect: | ||
* - Owner/Voting keys: To track masternode ownership and voting rights | ||
* - Payout scripts: To detect payments to specific addresses | ||
* - ProTx hashes: To track masternode lifecycle and updates | ||
* - Collateral outpoints: To track masternode collateral | ||
* - Credit outputs: To track platform-related transactions | ||
* | ||
* Each transaction type has different fields based on its purpose: | ||
* - ProRegTx: All identity and payout fields (initial registration) | ||
* - ProUpServTx: ProTx hash and operator payout (service updates) | ||
* - ProUpRegTx: ProTx hash, voting key, and payout (ownership updates) | ||
* - ProUpRevTx: ProTx hash only (revocation tracking) | ||
* - AssetLockTx: Credit output scripts (platform credits) | ||
*/ | ||
// Helper function to add a script to the filter if it's not empty | ||
static void AddScriptElement(const CScript& script, const std::function<void(Span<const unsigned char>)>& addElement) | ||
{ | ||
if (!script.empty()) { | ||
addElement(MakeUCharSpan(script)); | ||
} | ||
} | ||
|
||
// Helper function to add a hash/key to the filter | ||
template <typename T> | ||
static void AddHashElement(const T& hash, const std::function<void(Span<const unsigned char>)>& addElement) | ||
{ | ||
addElement(MakeUCharSpan(hash)); | ||
} | ||
|
||
// NOTE(maintenance): Keep this in sync with | ||
// CBloomFilter::CheckSpecialTransactionMatchesAndUpdate in | ||
// src/common/bloom.cpp. If you add or remove fields for a special | ||
// transaction type here, update the bloom filter routine accordingly | ||
// (and vice versa) to avoid compact-filter vs bloom-filter divergence. | ||
void ExtractSpecialTxFilterElements(const CTransaction& tx, const std::function<void(Span<const unsigned char>)>& addElement) | ||
{ | ||
if (!tx.HasExtraPayloadField()) { | ||
return; // not a special transaction | ||
} | ||
|
||
switch (tx.nType) { | ||
case TRANSACTION_PROVIDER_REGISTER: { | ||
knst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (const auto opt_proTx = GetTxPayload<CProRegTx>(tx)) { | ||
// Add collateral outpoint | ||
CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); | ||
stream << opt_proTx->collateralOutpoint; | ||
addElement(MakeUCharSpan(stream)); | ||
|
||
// Add owner key ID | ||
AddHashElement(opt_proTx->keyIDOwner, addElement); | ||
|
||
// Add voting key ID | ||
AddHashElement(opt_proTx->keyIDVoting, addElement); | ||
|
||
// Add payout script | ||
AddScriptElement(opt_proTx->scriptPayout, addElement); | ||
} | ||
break; | ||
} | ||
case TRANSACTION_PROVIDER_UPDATE_SERVICE: { | ||
if (const auto opt_proTx = GetTxPayload<CProUpServTx>(tx)) { | ||
// Add ProTx hash | ||
AddHashElement(opt_proTx->proTxHash, addElement); | ||
|
||
// Add operator payout script | ||
AddScriptElement(opt_proTx->scriptOperatorPayout, addElement); | ||
} | ||
break; | ||
} | ||
case TRANSACTION_PROVIDER_UPDATE_REGISTRAR: { | ||
if (const auto opt_proTx = GetTxPayload<CProUpRegTx>(tx)) { | ||
// Add ProTx hash | ||
AddHashElement(opt_proTx->proTxHash, addElement); | ||
|
||
// Add voting key ID | ||
AddHashElement(opt_proTx->keyIDVoting, addElement); | ||
|
||
// Add payout script | ||
AddScriptElement(opt_proTx->scriptPayout, addElement); | ||
} | ||
break; | ||
} | ||
case TRANSACTION_PROVIDER_UPDATE_REVOKE: { | ||
if (const auto opt_proTx = GetTxPayload<CProUpRevTx>(tx)) { | ||
// Add ProTx hash | ||
AddHashElement(opt_proTx->proTxHash, addElement); | ||
} | ||
break; | ||
} | ||
case TRANSACTION_ASSET_LOCK: { | ||
// Asset Lock transactions have special outputs (creditOutputs) that should be included | ||
if (const auto opt_assetlockTx = GetTxPayload<CAssetLockPayload>(tx)) { | ||
const auto& extraOuts = opt_assetlockTx->getCreditOutputs(); | ||
for (const CTxOut& txout : extraOuts) { | ||
const CScript& script = txout.scriptPubKey; | ||
// Exclude OP_RETURN outputs as they are not spendable | ||
if (!script.empty() && script[0] != OP_RETURN) { | ||
AddScriptElement(script, addElement); | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
case TRANSACTION_ASSET_UNLOCK: | ||
case TRANSACTION_COINBASE: | ||
case TRANSACTION_QUORUM_COMMITMENT: | ||
case TRANSACTION_MNHF_SIGNAL: | ||
// No additional special fields needed for these transaction types | ||
// Their standard outputs are already included in the base filter | ||
break; | ||
} // no default case, so the compiler can warn about missing cases | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Copyright (c) 2025 The Dash Core developers | ||
// Distributed under the MIT software license, see the accompanying | ||
// file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
|
||
#ifndef BITCOIN_EVO_SPECIALTX_FILTER_H | ||
#define BITCOIN_EVO_SPECIALTX_FILTER_H | ||
|
||
#include <functional> | ||
#include <span.h> | ||
|
||
class CTransaction; | ||
|
||
/** | ||
* Extract filterable elements from special transactions for use in compact block filters. | ||
* This function extracts the same fields that are included in bloom filters to ensure | ||
* SPV clients can detect special transactions using either filtering mechanism. | ||
* | ||
* @param tx The transaction to extract elements from | ||
* @param addElement Callback to add extracted elements to the filter. Uses | ||
* Span<const unsigned char> to avoid intermediate | ||
* allocations. | ||
*/ | ||
void ExtractSpecialTxFilterElements(const CTransaction& tx, | ||
const std::function<void(Span<const unsigned char>)>& addElement); | ||
|
||
#endif // BITCOIN_EVO_SPECIALTX_FILTER_H |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ using node::UndoReadFromDisk; | |
constexpr uint8_t DB_BLOCK_HASH{'s'}; | ||
constexpr uint8_t DB_BLOCK_HEIGHT{'t'}; | ||
constexpr uint8_t DB_FILTER_POS{'P'}; | ||
constexpr uint8_t DB_VERSION{'V'}; | ||
|
||
constexpr unsigned int MAX_FLTR_FILE_SIZE = 0x1000000; // 16 MiB | ||
/** The pre-allocation chunk size for fltr?????.dat files */ | ||
|
@@ -111,11 +112,34 @@ BlockFilterIndex::BlockFilterIndex(BlockFilterType filter_type, | |
|
||
m_name = filter_name + " block filter index"; | ||
m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, f_wipe); | ||
|
||
// Check version | ||
int version = 0; | ||
if (!m_db->Read(DB_VERSION, version) || version < CURRENT_VERSION) { | ||
// No version or too old version means we need to start from scratch | ||
LogPrintf("%s: Outdated or no version blockfilter, starting from scratch\n", __func__); | ||
m_db.reset(); | ||
m_db = std::make_unique<BaseIndex::DB>(path / "db", n_cache_size, f_memory, /*f_wipe=*/true); | ||
m_db->Write(DB_VERSION, CURRENT_VERSION); | ||
} | ||
|
||
m_filter_fileseq = std::make_unique<FlatFileSeq>(std::move(path), "fltr", FLTR_FILE_CHUNK_SIZE); | ||
} | ||
|
||
bool BlockFilterIndex::Init() | ||
{ | ||
// Check version compatibility first | ||
int version = 0; | ||
if (m_db->Exists(DB_VERSION)) { | ||
if (!m_db->Read(DB_VERSION, version)) { | ||
return error("%s: Failed to read %s index version from database", __func__, GetName()); | ||
} | ||
if (version > CURRENT_VERSION) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why exactly do we need to check DB_VERSION twice - once in a constructor
Init is called externally when object already exist and check & reset of db is already done, isn't it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see nothing in BlockFilterIndex::BlockFilterIndex that checks version There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In ctor we check for an outdated version. Here we check if a version we read is from the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could probably drop There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
could it be like that in ctor and that's all? @UdjinM6 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It depends. Do we want to recreate it if user started an older version temporary (will have to recreate it once again when he is back to the newer one) or do we want to just quit (he can disable filter for some time to be able to run an older node)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, it seems as current solution is fine then |
||
return error("%s: %s index version %d is too high (expected <= %d)", | ||
__func__, GetName(), version, CURRENT_VERSION); | ||
} | ||
} | ||
|
||
if (!m_db->Read(DB_FILTER_POS, m_next_filter_pos)) { | ||
// Check that the cause of the read failure is that the key does not exist. Any other errors | ||
// indicate database corruption or a disk failure, and starting the index would cause | ||
|
@@ -136,6 +160,11 @@ bool BlockFilterIndex::CommitInternal(CDBBatch& batch) | |
{ | ||
const FlatFilePos& pos = m_next_filter_pos; | ||
|
||
// Write the current version if this is a new index | ||
if (!m_db->Exists(DB_VERSION)) { | ||
batch.Write(DB_VERSION, CURRENT_VERSION); | ||
} | ||
|
||
// Flush current filter file to disk. | ||
CAutoFile file(m_filter_fileseq->Open(pos), SER_DISK, CLIENT_VERSION); | ||
if (file.IsNull()) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Document exactly which elements are added (and OP_RETURN exclusion).
Release notes should state which special-transaction fields are fed into filters, matching bloom-filter parity. Add a concise bullet block.
Please verify the exact field list matches the implementation in evo/specialtx_filter.{h,cpp}.
🏁 Script executed:
Length of output: 37859
🏁 Script executed:
Length of output: 5243
Add detailed extraction list to release notes
To ensure the release notes accurately reflect which fields are indexed (and the OP_RETURN exclusion), please update
doc/release-notes-6825.md
(around lines 7–14) by inserting the following block immediately after the list of transaction types:This exactly matches the implementation in
src/evo/specialtx_filter.cpp
.🧰 Tools
🪛 LanguageTool
[grammar] ~7-~7: There might be a mistake here.
Context: ...s fields from Dash special transactions: - ProRegTx (masternode registration) - P...
(QB_NEW_EN)
[grammar] ~8-~8: There might be a mistake here.
Context: ...: - ProRegTx (masternode registration) - ProUpServTx (masternode service updates)...
(QB_NEW_EN)
[grammar] ~9-~9: There might be a mistake here.
Context: ...ProUpServTx (masternode service updates) - ProUpRegTx (masternode operator updates)...
(QB_NEW_EN)
[grammar] ~10-~10: There might be a mistake here.
Context: ...ProUpRegTx (masternode operator updates) - ProUpRevTx (masternode revocation) - A...
(QB_NEW_EN)
[grammar] ~11-~11: There might be a mistake here.
Context: ...) - ProUpRevTx (masternode revocation) - AssetLockTx (platform credit outputs) - ...
(QB_NEW_EN)
[grammar] ~12-~12: There might be a mistake here.
Context: ... - AssetLockTx (platform credit outputs) - A versioning system has been added to de...
(QB_NEW_EN)
🤖 Prompt for AI Agents