Skip to content

Commit b5a7c0a

Browse files
authored
fix: require chainlock signatures only after V20 activation (#405)
Pre-V20 blocks have no chainlock signatures in `MnListDiff` responses, but we currently incorrectly trigger failures when they are missing below v20 activation height. V20 activation heights (from Dash Core src/chainparams.cpp): - Mainnet: 1,987,776 - Testnet: 905,100 Added `v20_activation_height()` to `NetworkExt` and check in `apply_diff` and `from_diff` to only require signatures for post-V20 blocks. See DIP-0029.
1 parent 2d3a531 commit b5a7c0a

File tree

4 files changed

+241
-22
lines changed

4 files changed

+241
-22
lines changed

dash/src/network/constants.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ pub const PROTOCOL_VERSION: u32 = 70237;
6969
pub trait NetworkExt {
7070
/// The known dash genesis block hash for mainnet and testnet
7171
fn known_genesis_block_hash(&self) -> Option<BlockHash>;
72+
73+
/// V20 activation height when quorumsCLSigs was introduced (protocol 70230).
74+
/// See DIP-0029 and Dash Core src/chainparams.cpp.
75+
fn v20_activation_height(&self) -> u32;
7276
}
7377

7478
impl NetworkExt for Network {
@@ -99,6 +103,15 @@ impl NetworkExt for Network {
99103
_ => None,
100104
}
101105
}
106+
107+
fn v20_activation_height(&self) -> u32 {
108+
match self {
109+
Network::Dash => 1_987_776,
110+
Network::Testnet => 905_100,
111+
// Devnet and regtest activate V20 immediately
112+
_ => 0,
113+
}
114+
}
102115
}
103116

104117
/// Flags to indicate which network services a node supports.

dash/src/sml/masternode_list/apply_diff.rs

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::bls_sig_utils::BLSSignature;
2+
use crate::network::constants::NetworkExt;
23
use crate::network::message_sml::MnListDiff;
34
use crate::prelude::CoreBlockHeight;
45
use crate::sml::error::SmlError;
@@ -9,6 +10,7 @@ use crate::sml::masternode_list::MasternodeList;
910
use crate::sml::quorum_entry::qualified_quorum_entry::{
1011
QualifiedQuorumEntry, VerifyingChainLockSignaturesType,
1112
};
13+
use dash_network::Network;
1214

1315
impl MasternodeList {
1416
/// Applies an `MnListDiff` to update the current masternode list.
@@ -40,6 +42,7 @@ impl MasternodeList {
4042
diff: MnListDiff,
4143
diff_end_height: CoreBlockHeight,
4244
previous_chain_lock_sigs: Option<[BLSSignature; 3]>,
45+
network: Network,
4346
) -> Result<(MasternodeList, Option<BLSSignature>), SmlError> {
4447
// Ensure the base block hash matches
4548
if self.block_hash != diff.base_block_hash {
@@ -89,8 +92,12 @@ impl MasternodeList {
8992
}
9093
}
9194

92-
// Verify all slots have been filled
93-
if quorum_sig_lookup.iter().any(Option::is_none) {
95+
// quorumsCLSigs only exists after V20 activation (protocol 70230).
96+
// Pre-V20 blocks have no chainlock signatures. See DIP-0029.
97+
let signatures_available = !quorum_sig_lookup.iter().any(Option::is_none);
98+
let signatures_required = diff_end_height >= network.v20_activation_height();
99+
100+
if signatures_required && !signatures_available {
94101
return Err(SmlError::IncompleteSignatureSet);
95102
}
96103

@@ -105,37 +112,40 @@ impl MasternodeList {
105112
let entry_hash = new_quorum.calculate_entry_hash();
106113
let verifying_chain_lock_signature =
107114
if new_quorum.llmq_type.is_rotating_quorum_type() {
108-
if rotating_sig.is_none() {
109-
if let Some(sig) = quorum_sig_lookup[idx] {
110-
rotating_sig = Some(*sig)
111-
} else {
112-
return Err(SmlError::IncompleteSignatureSet);
113-
}
115+
if rotating_sig.is_none()
116+
&& let Some(sig) = quorum_sig_lookup.get(idx).copied().flatten()
117+
{
118+
rotating_sig = Some(*sig);
114119
}
115-
if let Some(previous_chain_lock_sigs) = previous_chain_lock_sigs {
116-
if let Some(sig) = quorum_sig_lookup[idx] {
117-
Some(VerifyingChainLockSignaturesType::Rotating([
118-
previous_chain_lock_sigs[0],
119-
previous_chain_lock_sigs[1],
120-
previous_chain_lock_sigs[2],
121-
*sig,
122-
]))
120+
if signatures_available {
121+
if let Some(previous_chain_lock_sigs) = previous_chain_lock_sigs {
122+
quorum_sig_lookup.get(idx).copied().flatten().map(|sig| {
123+
VerifyingChainLockSignaturesType::Rotating([
124+
previous_chain_lock_sigs[0],
125+
previous_chain_lock_sigs[1],
126+
previous_chain_lock_sigs[2],
127+
*sig,
128+
])
129+
})
123130
} else {
124-
return Err(SmlError::IncompleteSignatureSet);
131+
None
125132
}
126133
} else {
127134
None
128135
}
129136
} else {
130-
quorum_sig_lookup[idx]
137+
quorum_sig_lookup
138+
.get(idx)
139+
.copied()
140+
.flatten()
131141
.copied()
132142
.map(VerifyingChainLockSignaturesType::NonRotating)
133143
};
134144
QualifiedQuorumEntry {
135145
quorum_entry: new_quorum,
136146
verified: LLMQEntryVerificationStatus::Skipped(
137147
LLMQEntryVerificationSkipStatus::NotMarkedForVerification,
138-
), // Default to unverified
148+
),
139149
commitment_hash,
140150
entry_hash,
141151
verifying_chain_lock_signature,
@@ -155,3 +165,80 @@ impl MasternodeList {
155165
Ok((builder.build(), rotating_sig))
156166
}
157167
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::*;
172+
use crate::consensus::deserialize;
173+
use crate::network::constants::NetworkExt;
174+
use crate::sml::masternode_list::from_diff::TryFromWithBlockHashLookup;
175+
176+
#[test]
177+
fn apply_diff_post_v20_requires_chainlock_signatures() {
178+
// Create base list from first diff
179+
let base_diff_bytes: &[u8] =
180+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_0_2227096.bin");
181+
let base_diff: MnListDiff = deserialize(base_diff_bytes).expect("expected to deserialize");
182+
183+
let base_list = MasternodeList::try_from_with_block_hash_lookup(
184+
base_diff,
185+
|_| Some(2_227_096),
186+
Network::Dash,
187+
)
188+
.expect("expected to create base list");
189+
190+
// Load second diff and clear signatures
191+
let diff_bytes: &[u8] =
192+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_2227096_2241332.bin");
193+
let mut diff: MnListDiff = deserialize(diff_bytes).expect("expected to deserialize");
194+
diff.quorums_chainlock_signatures.clear();
195+
196+
// Height 2241332 is post-V20 on mainnet (1,987,776)
197+
let post_v20_height = 2_241_332;
198+
assert!(post_v20_height >= Network::Dash.v20_activation_height());
199+
200+
let result = base_list.apply_diff(diff, post_v20_height, None, Network::Dash);
201+
202+
assert!(
203+
matches!(result, Err(SmlError::IncompleteSignatureSet)),
204+
"Post-V20 apply_diff should require chainlock signatures"
205+
);
206+
}
207+
208+
#[test]
209+
fn apply_diff_pre_v20_allows_missing_chainlock_signatures() {
210+
// Create base list from first diff at pre-V20 height
211+
let base_diff_bytes: &[u8] =
212+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_0_2227096.bin");
213+
let base_diff: MnListDiff = deserialize(base_diff_bytes).expect("expected to deserialize");
214+
215+
let base_height = 1_800_000u32;
216+
let base_list = MasternodeList::try_from_with_block_hash_lookup(
217+
base_diff,
218+
|_| Some(base_height),
219+
Network::Dash,
220+
)
221+
.expect("expected to create base list");
222+
223+
// Load second diff and clear signatures
224+
let diff_bytes: &[u8] =
225+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_2227096_2241332.bin");
226+
let mut diff: MnListDiff = deserialize(diff_bytes).expect("expected to deserialize");
227+
228+
// Fix base_block_hash to match our base list
229+
diff.base_block_hash = base_list.block_hash;
230+
diff.quorums_chainlock_signatures.clear();
231+
232+
// Use a pre-V20 height on mainnet
233+
let pre_v20_height = 1_900_000u32;
234+
assert!(pre_v20_height < Network::Dash.v20_activation_height());
235+
236+
let result = base_list.apply_diff(diff, pre_v20_height, None, Network::Dash);
237+
238+
assert!(
239+
result.is_ok(),
240+
"Pre-V20 apply_diff should allow missing chainlock signatures: {:?}",
241+
result.err()
242+
);
243+
}
244+
}

dash/src/sml/masternode_list/from_diff.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,11 @@ impl TryFromWithBlockHashLookup<MnListDiff> for MasternodeList {
107107
}
108108
}
109109

110-
// Verify all slots have been filled
111-
if quorum_sig_lookup.iter().any(Option::is_none) {
110+
// quorumsCLSigs only exists after V20 activation (protocol 70230).
111+
// Pre-V20 blocks have no chainlock signatures. See DIP-0029.
112+
if known_height >= network.v20_activation_height()
113+
&& quorum_sig_lookup.iter().any(Option::is_none)
114+
{
112115
return Err(SmlError::IncompleteSignatureSet);
113116
}
114117

@@ -127,7 +130,10 @@ impl TryFromWithBlockHashLookup<MnListDiff> for MasternodeList {
127130
),
128131
commitment_hash,
129132
entry_hash,
130-
verifying_chain_lock_signature: quorum_sig_lookup[idx]
133+
verifying_chain_lock_signature: quorum_sig_lookup
134+
.get(idx)
135+
.copied()
136+
.flatten()
131137
.copied()
132138
.map(VerifyingChainLockSignaturesType::NonRotating),
133139
}
@@ -147,3 +153,63 @@ impl TryFromWithBlockHashLookup<MnListDiff> for MasternodeList {
147153
})
148154
}
149155
}
156+
157+
#[cfg(test)]
158+
mod tests {
159+
use super::*;
160+
use crate::consensus::deserialize;
161+
use crate::network::constants::NetworkExt;
162+
163+
#[test]
164+
fn post_v20_requires_chainlock_signatures() {
165+
let mn_list_diff_bytes: &[u8] =
166+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_0_2227096.bin");
167+
let mut diff: MnListDiff =
168+
deserialize(mn_list_diff_bytes).expect("expected to deserialize");
169+
170+
// Clear signatures to simulate missing data
171+
diff.quorums_chainlock_signatures.clear();
172+
173+
// Height 2227096 is post-V20 on mainnet (1,987,776)
174+
let post_v20_height = 2_227_096;
175+
assert!(post_v20_height >= Network::Dash.v20_activation_height());
176+
177+
let result = MasternodeList::try_from_with_block_hash_lookup(
178+
diff,
179+
|_| Some(post_v20_height),
180+
Network::Dash,
181+
);
182+
183+
assert!(
184+
matches!(result, Err(SmlError::IncompleteSignatureSet)),
185+
"Post-V20 blocks should require chainlock signatures"
186+
);
187+
}
188+
189+
#[test]
190+
fn pre_v20_allows_missing_chainlock_signatures() {
191+
let mn_list_diff_bytes: &[u8] =
192+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_0_2227096.bin");
193+
let mut diff: MnListDiff =
194+
deserialize(mn_list_diff_bytes).expect("expected to deserialize");
195+
196+
// Clear signatures to simulate pre-V20 data
197+
diff.quorums_chainlock_signatures.clear();
198+
199+
// Use a pre-V20 height on mainnet (V20 at 1,987,776)
200+
let pre_v20_height = 1_900_000;
201+
assert!(pre_v20_height < Network::Dash.v20_activation_height());
202+
203+
let result = MasternodeList::try_from_with_block_hash_lookup(
204+
diff,
205+
|_| Some(pre_v20_height),
206+
Network::Dash,
207+
);
208+
209+
assert!(
210+
result.is_ok(),
211+
"Pre-V20 blocks should allow missing chainlock signatures: {:?}",
212+
result.err()
213+
);
214+
}
215+
}

dash/src/sml/masternode_list_engine/mod.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,7 @@ impl MasternodeListEngine {
909909
masternode_list_diff.clone(),
910910
diff_end_height,
911911
previous_chain_lock_sigs,
912+
self.network,
912913
)?;
913914
if verify_quorums {
914915
// We should go through all quorums of the masternode list to update those that were not yet verified
@@ -978,6 +979,7 @@ impl MasternodeListEngine {
978979
masternode_list_diff.clone(),
979980
diff_end_height,
980981
None,
982+
self.network,
981983
)?;
982984
if verify_quorums {
983985
return Err(SmlError::FeatureNotTurnedOn(
@@ -1436,4 +1438,55 @@ mod tests {
14361438
.expect("expected to validated quorums");
14371439
}
14381440
}
1441+
1442+
#[test]
1443+
fn feed_qr_info_rejects_post_v20_with_missing_chainlock_signatures() {
1444+
let mn_list_diff_bytes: &[u8] =
1445+
include_bytes!("../../../tests/data/test_DML_diffs/mn_list_diff_0_2227096.bin");
1446+
let diff: MnListDiff = deserialize(mn_list_diff_bytes).expect("expected to deserialize");
1447+
let mut masternode_list_engine =
1448+
MasternodeListEngine::initialize_with_diff_to_height(diff, 2227096, Network::Dash)
1449+
.expect("expected to start engine");
1450+
1451+
let block_container_bytes: &[u8] =
1452+
include_bytes!("../../../tests/data/test_DML_diffs/block_container_2240504.dat");
1453+
let block_container: MasternodeListEngineBlockContainer =
1454+
bincode::decode_from_slice(block_container_bytes, bincode::config::standard())
1455+
.expect("expected to decode")
1456+
.0;
1457+
let mn_list_diffs_bytes: &[u8] =
1458+
include_bytes!("../../../tests/data/test_DML_diffs/mnlistdiffs_2240504.dat");
1459+
let mn_list_diffs: BTreeMap<(CoreBlockHeight, CoreBlockHeight), MnListDiff> =
1460+
bincode::decode_from_slice(mn_list_diffs_bytes, bincode::config::standard())
1461+
.expect("expected to decode")
1462+
.0;
1463+
let qr_info_bytes: &[u8] =
1464+
include_bytes!("../../../tests/data/test_DML_diffs/qrinfo_2240504.dat");
1465+
let mut qr_info: QRInfo =
1466+
bincode::decode_from_slice(qr_info_bytes, bincode::config::standard())
1467+
.expect("expected to decode")
1468+
.0;
1469+
1470+
masternode_list_engine.block_container = block_container;
1471+
1472+
for ((_start_height, height), diff) in mn_list_diffs.into_iter() {
1473+
masternode_list_engine
1474+
.apply_diff(diff, Some(height), false, None)
1475+
.expect("expected to apply diff");
1476+
}
1477+
1478+
// Clear chainlock signatures to simulate missing data for post-V20 block
1479+
qr_info.mn_list_diff_at_h_minus_2c.quorums_chainlock_signatures.clear();
1480+
1481+
// feed_qr_info should fail for post-V20 blocks with missing signatures
1482+
let result = masternode_list_engine
1483+
.feed_qr_info::<fn(&BlockHash) -> Result<u32, ClientDataRetrievalError>>(
1484+
qr_info, false, false, None,
1485+
);
1486+
1487+
assert!(
1488+
result.is_err(),
1489+
"Post-V20 feed_qr_info should reject missing chainlock signatures"
1490+
);
1491+
}
14391492
}

0 commit comments

Comments
 (0)