Skip to content

Commit f3dbb8c

Browse files
authored
Merge pull request #3225 from ProvableHQ/fix/sync-invalid-subdag
[Fix] Check subDAG atomicity correctly for a chain of pending blocks
2 parents ced16d7 + d94b131 commit f3dbb8c

4 files changed

Lines changed: 64 additions & 5 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ jobs:
775775

776776
ledger-narwhal-data:
777777
executor: rust-docker
778-
resource_class: << pipeline.parameters.small >>
778+
resource_class: << pipeline.parameters.medium >>
779779
steps:
780780
- run_test:
781781
workspace_member: snarkvm-ledger-narwhal-data

ledger/src/check_next_block.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
112112
fn check_block_subdag_inner(&self, block: &Block<N>, prefix: &[PendingBlock<N>]) -> Result<(), CheckBlockError<N>> {
113113
// Grab a lock to the latest_block in the ledger, to prevent concurrent writes to the ledger,
114114
// and to ensure that this check is atomic.
115+
//
116+
// Note: The latest block in the ledger is not necessarily the direct predecessor of `block`.
117+
// If `prefix` is non-empty the direct predecessor is the last entry in the prefix.
115118
let latest_block = self.current_block.read();
116119

117120
// First check that the heights and hashes of the pending block sequence and of the new block are correct.
@@ -149,8 +152,10 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
149152
// Ensure the certificates in the block subdag have met quorum requirements.
150153
self.check_block_subdag_quorum(block)?;
151154

152-
// Determine if the block subdag is correctly constructed and is not a combination of multiple subdags.
153-
self.check_block_subdag_atomicity(block, &latest_block)?;
155+
// Check subDAG atomicity against the latest block in the prefix.
156+
// Only if the prefix is empty, check against the latest block in the ledger.
157+
let predecessor = prefix.last().map_or(&*latest_block, |b| &**b);
158+
self.check_block_subdag_atomicity(block, predecessor)?;
154159

155160
// Ensure that all leaves of the subdag point to valid batches in other subdags/blocks.
156161
self.check_block_subdag_leaves(block, prefix)?;

ledger/tests/helpers/mod.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,13 @@ impl TestChainBuilder {
7676
let genesis_rng = &mut TestRng::from_seed(seed);
7777
let genesis_block = VM::from(store).unwrap().genesis_beacon(&private_key, genesis_rng).unwrap();
7878

79-
// Extract the private keys from the genesis committee by using the same RNG to sample private keys.
79+
// Reconstruct the private keys of the genesis committee. genesis_beacon uses `private_key`
80+
// as the first member, then samples (committee_size - 1) more from the seeded RNG.
8081
let genesis_rng = &mut TestRng::from_seed(seed);
81-
let private_keys = (0..committee_size).map(|_| PrivateKey::new(genesis_rng).unwrap()).collect();
82+
let mut private_keys = vec![private_key];
83+
for _ in 1..committee_size {
84+
private_keys.push(PrivateKey::new(genesis_rng).unwrap());
85+
}
8286

8387
Self::from_genesis(private_keys, genesis_block)
8488
}
@@ -297,4 +301,16 @@ impl TestChainBuilder {
297301
pub fn genesis_block(&self) -> &Block<CurrentNetwork> {
298302
&self.genesis_block
299303
}
304+
305+
/// Returns the index into `private_keys` of the elected leader for the given round,
306+
/// or `None` if the round has no committee or the leader is not among the known keys.
307+
pub fn get_leader_index(&self, round: u64) -> Option<usize> {
308+
let committee = self.ledger.get_committee_lookback_for_round(round).ok()??;
309+
let leader = committee.get_leader(round).ok()?;
310+
self.private_keys
311+
.iter()
312+
.enumerate()
313+
.find(|(_, key)| Address::try_from(*key).unwrap() == leader)
314+
.map(|(idx, _)| idx)
315+
}
300316
}

ledger/tests/pending_blocks.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,44 @@ fn test_prefix_with_duplicate_block_error() {
132132
assert!(matches!(*error, CheckBlockError::InvalidHeight { expected: 2, actual: 3 }));
133133
}
134134

135+
/// Regression test for the bug where `check_block_subdag_atomicity` used the raw ledger
136+
/// tip round as `latest_round` instead of the last prefix block's round when a non-empty
137+
/// prefix was provided.
138+
#[test]
139+
fn test_atomicity_check_uses_prefix_latest_block() {
140+
let rng = &mut TestRng::default();
141+
let mut builder = TestChainBuilder::new(4, rng);
142+
143+
// External ledger starts at genesis and is never explicitly advanced in this test.
144+
let ledger = Ledger::<CurrentNetwork, LedgerType<CurrentNetwork>>::load(
145+
builder.genesis_block().clone(),
146+
StorageMode::new_test(None),
147+
)
148+
.unwrap();
149+
150+
// Identify the elected leader at round 2 and exclude them from block 1.
151+
// This guarantees their certificate at round 2 is absent from block 1's subdag,
152+
// making it a late-arriving *leader* certificate that will appear in block 2's subdag.
153+
let skip_idx = builder.get_leader_index(2).expect("Leader not found for round 2");
154+
let block1 =
155+
builder.generate_block_with_opts(&BlockOptions { skip_nodes: vec![skip_idx], ..Default::default() }, rng);
156+
157+
// Pre-process block 1 against the external ledger (still at genesis) so it can
158+
// be used as a prefix when validating block 2.
159+
let pending_block1 = ledger.check_block_subdag(block1, &[]).unwrap();
160+
161+
// Generate block 2 with all validators participating. The previously skipped
162+
// validator's certificates for rounds ≤ block1.anchor_round are now included in
163+
// block 2's subdag as late-arriving entries.
164+
let block2 = builder.generate_block(rng);
165+
166+
// Block 2 must be accepted with block 1 as the prefix while the external ledger
167+
// is still at genesis. This would fail before the fix because the atomicity check
168+
// incorrectly used genesis's round (0) instead of block 1's anchor round, causing it
169+
// to flag the late-arriving leader cert at round 2 as a protocol violation.
170+
ledger.check_block_subdag(block2, &[pending_block1]).unwrap();
171+
}
172+
135173
#[test]
136174
fn test_check_block_content_invalid_height() {
137175
let rng = &mut TestRng::default();

0 commit comments

Comments
 (0)