|
1 |
| -use bdk_core::CheckPoint; |
| 1 | +use bdk_core::{CheckPoint, ToBlockHash}; |
2 | 2 | use bdk_testenv::{block_id, hash};
|
3 | 3 | use bitcoin::BlockHash;
|
4 | 4 |
|
@@ -55,3 +55,391 @@ fn checkpoint_destruction_is_sound() {
|
55 | 55 | }
|
56 | 56 | assert_eq!(cp.iter().count() as u32, end);
|
57 | 57 | }
|
| 58 | + |
| 59 | +// Custom struct for testing with prev_blockhash |
| 60 | +#[derive(Debug, Clone, Copy)] |
| 61 | +struct TestBlock { |
| 62 | + blockhash: BlockHash, |
| 63 | + prev_blockhash: BlockHash, |
| 64 | +} |
| 65 | + |
| 66 | +impl ToBlockHash for TestBlock { |
| 67 | + fn to_blockhash(&self) -> BlockHash { |
| 68 | + self.blockhash |
| 69 | + } |
| 70 | + |
| 71 | + fn prev_blockhash(&self) -> Option<BlockHash> { |
| 72 | + Some(self.prev_blockhash) |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +/// Test inserting data with conflicting prev_blockhash should displace checkpoint and create |
| 77 | +/// placeholder. |
| 78 | +/// |
| 79 | +/// When inserting data at height `h` with a `prev_blockhash` that conflicts with the checkpoint |
| 80 | +/// at height `h-1`, the checkpoint at `h-1` should be displaced and replaced with a placeholder |
| 81 | +/// containing the `prev_blockhash` from the inserted data. |
| 82 | +/// |
| 83 | +/// Expected: Checkpoint at 99 gets displaced when inserting at 100 with conflicting prev_blockhash. |
| 84 | +#[test] |
| 85 | +fn checkpoint_insert_conflicting_prev_blockhash() { |
| 86 | + // Create initial checkpoint at height 99 |
| 87 | + let block_99 = TestBlock { |
| 88 | + blockhash: hash!("block_at_99"), |
| 89 | + prev_blockhash: hash!("block_at_98"), |
| 90 | + }; |
| 91 | + let cp = CheckPoint::new(99, block_99); |
| 92 | + |
| 93 | + // The initial chain has a placeholder at 98 (from block_99's prev_blockhash) |
| 94 | + assert_eq!(cp.iter().count(), 2); |
| 95 | + let height_98_before = cp.get(98).expect("should have checkpoint at 98"); |
| 96 | + assert_eq!(height_98_before.hash(), block_99.prev_blockhash); |
| 97 | + assert!( |
| 98 | + height_98_before.data_ref().is_none(), |
| 99 | + "98 should be placeholder initially" |
| 100 | + ); |
| 101 | + |
| 102 | + // Insert data at height 100 with a prev_blockhash that conflicts with checkpoint at 99 |
| 103 | + let block_100_conflicting = TestBlock { |
| 104 | + blockhash: hash!("block_at_100"), |
| 105 | + prev_blockhash: hash!("different_block_at_99"), // Conflicts with block_99.blockhash |
| 106 | + }; |
| 107 | + |
| 108 | + let result = cp.insert(100, block_100_conflicting); |
| 109 | + |
| 110 | + // Expected behavior: The checkpoint at 99 should be displaced and replaced with a placeholder |
| 111 | + let height_99_after = result.get(99).expect("checkpoint at 99 should exist"); |
| 112 | + assert_eq!( |
| 113 | + height_99_after.hash(), |
| 114 | + block_100_conflicting.prev_blockhash, |
| 115 | + "checkpoint at 99 should be displaced and have the prev_blockhash from inserted data" |
| 116 | + ); |
| 117 | + assert!( |
| 118 | + height_99_after.data_ref().is_none(), |
| 119 | + "checkpoint at 99 should be a placeholder (no data) after displacement" |
| 120 | + ); |
| 121 | + |
| 122 | + // The checkpoint at 100 should be inserted correctly |
| 123 | + let height_100 = result.get(100).expect("checkpoint at 100 should exist"); |
| 124 | + assert_eq!(height_100.hash(), block_100_conflicting.blockhash); |
| 125 | + assert!( |
| 126 | + height_100.data_ref().is_some(), |
| 127 | + "checkpoint at 100 should have data" |
| 128 | + ); |
| 129 | + |
| 130 | + // Verify chain structure |
| 131 | + assert_eq!(result.height(), 100, "tip should be at height 100"); |
| 132 | + assert_eq!( |
| 133 | + result.iter().count(), |
| 134 | + 3, |
| 135 | + "should have 3 checkpoints (98 placeholder, 99 placeholder, 100)" |
| 136 | + ); |
| 137 | +} |
| 138 | + |
| 139 | +/// Test inserting data that conflicts with prev_blockhash of higher checkpoints should purge them. |
| 140 | +/// |
| 141 | +/// When inserting data at height `h` where the blockhash conflicts with the `prev_blockhash` of |
| 142 | +/// checkpoint at height `h+1`, the checkpoint at `h+1` and all checkpoints above it should be |
| 143 | +/// purged from the chain. |
| 144 | +/// |
| 145 | +/// Expected: Checkpoints at 100, 101, 102 get purged when inserting at 99 with conflicting |
| 146 | +/// blockhash. |
| 147 | +#[test] |
| 148 | +fn checkpoint_insert_purges_conflicting_tail() { |
| 149 | + // Create a chain with multiple checkpoints |
| 150 | + let block_98 = TestBlock { |
| 151 | + blockhash: hash!("block_at_98"), |
| 152 | + prev_blockhash: hash!("block_at_97"), |
| 153 | + }; |
| 154 | + let block_99 = TestBlock { |
| 155 | + blockhash: hash!("block_at_99"), |
| 156 | + prev_blockhash: hash!("block_at_98"), |
| 157 | + }; |
| 158 | + let block_100 = TestBlock { |
| 159 | + blockhash: hash!("block_at_100"), |
| 160 | + prev_blockhash: hash!("block_at_99"), |
| 161 | + }; |
| 162 | + let block_101 = TestBlock { |
| 163 | + blockhash: hash!("block_at_101"), |
| 164 | + prev_blockhash: hash!("block_at_100"), |
| 165 | + }; |
| 166 | + let block_102 = TestBlock { |
| 167 | + blockhash: hash!("block_at_102"), |
| 168 | + prev_blockhash: hash!("block_at_101"), |
| 169 | + }; |
| 170 | + |
| 171 | + let cp = CheckPoint::from_blocks(vec![ |
| 172 | + (98, block_98), |
| 173 | + (99, block_99), |
| 174 | + (100, block_100), |
| 175 | + (101, block_101), |
| 176 | + (102, block_102), |
| 177 | + ]) |
| 178 | + .expect("should create valid checkpoint chain"); |
| 179 | + |
| 180 | + // Verify initial chain has all checkpoints |
| 181 | + assert_eq!(cp.iter().count(), 6); // 97 (placeholder), 98, 99, 100, 101, 102 |
| 182 | + |
| 183 | + // Insert a conflicting block at height 99 |
| 184 | + // The new block's hash will conflict with block_100's prev_blockhash |
| 185 | + let conflicting_block_99 = TestBlock { |
| 186 | + blockhash: hash!("different_block_at_99"), |
| 187 | + prev_blockhash: hash!("block_at_98"), // Matches existing block_98 |
| 188 | + }; |
| 189 | + |
| 190 | + let result = cp.insert(99, conflicting_block_99); |
| 191 | + |
| 192 | + // Expected: Heights 100, 101, 102 should be purged because block_100's |
| 193 | + // prev_blockhash conflicts with the new block_99's hash |
| 194 | + assert_eq!( |
| 195 | + result.height(), |
| 196 | + 99, |
| 197 | + "tip should be at height 99 after purging higher checkpoints" |
| 198 | + ); |
| 199 | + |
| 200 | + // Check that only 98 and 99 remain (plus placeholder at 97) |
| 201 | + assert_eq!( |
| 202 | + result.iter().count(), |
| 203 | + 3, |
| 204 | + "should have 3 checkpoints (97 placeholder, 98, 99)" |
| 205 | + ); |
| 206 | + |
| 207 | + // Verify height 99 has the new conflicting block |
| 208 | + let height_99 = result.get(99).expect("checkpoint at 99 should exist"); |
| 209 | + assert_eq!(height_99.hash(), conflicting_block_99.blockhash); |
| 210 | + assert!( |
| 211 | + height_99.data_ref().is_some(), |
| 212 | + "checkpoint at 99 should have data" |
| 213 | + ); |
| 214 | + |
| 215 | + // Verify height 98 remains unchanged |
| 216 | + let height_98 = result.get(98).expect("checkpoint at 98 should exist"); |
| 217 | + assert_eq!(height_98.hash(), block_98.blockhash); |
| 218 | + assert!( |
| 219 | + height_98.data_ref().is_some(), |
| 220 | + "checkpoint at 98 should have data" |
| 221 | + ); |
| 222 | + |
| 223 | + // Verify heights 100, 101, 102 are purged |
| 224 | + assert!( |
| 225 | + result.get(100).is_none(), |
| 226 | + "checkpoint at 100 should be purged" |
| 227 | + ); |
| 228 | + assert!( |
| 229 | + result.get(101).is_none(), |
| 230 | + "checkpoint at 101 should be purged" |
| 231 | + ); |
| 232 | + assert!( |
| 233 | + result.get(102).is_none(), |
| 234 | + "checkpoint at 102 should be purged" |
| 235 | + ); |
| 236 | +} |
| 237 | + |
| 238 | +/// Test inserting between checkpoints with conflicts on both sides. |
| 239 | +/// |
| 240 | +/// When inserting at height between two checkpoints where the inserted data's `prev_blockhash` |
| 241 | +/// conflicts with the lower checkpoint and its `blockhash` conflicts with the upper checkpoint's |
| 242 | +/// `prev_blockhash`, both checkpoints should be handled: lower displaced, upper purged. |
| 243 | +/// |
| 244 | +/// Expected: Checkpoint at 4 displaced with placeholder, checkpoint at 6 purged. |
| 245 | +#[test] |
| 246 | +fn checkpoint_insert_between_conflicting_both_sides() { |
| 247 | + // Create checkpoints at heights 4 and 6 |
| 248 | + let block_4 = TestBlock { |
| 249 | + blockhash: hash!("block_at_4"), |
| 250 | + prev_blockhash: hash!("block_at_3"), |
| 251 | + }; |
| 252 | + let block_6 = TestBlock { |
| 253 | + blockhash: hash!("block_at_6"), |
| 254 | + prev_blockhash: hash!("block_at_5_original"), // This will conflict with inserted block 5 |
| 255 | + }; |
| 256 | + |
| 257 | + let cp = CheckPoint::from_blocks(vec![(4, block_4), (6, block_6)]) |
| 258 | + .expect("should create valid checkpoint chain"); |
| 259 | + |
| 260 | + // Verify initial state |
| 261 | + assert_eq!(cp.iter().count(), 4); // 3 (placeholder), 4, 5 (placeholder from 6's prev), 6 |
| 262 | + |
| 263 | + // Insert at height 5 with conflicts on both sides |
| 264 | + let block_5_conflicting = TestBlock { |
| 265 | + blockhash: hash!("block_at_5_new"), // Conflicts with block_6.prev_blockhash |
| 266 | + prev_blockhash: hash!("different_block_at_4"), // Conflicts with block_4.blockhash |
| 267 | + }; |
| 268 | + |
| 269 | + let result = cp.insert(5, block_5_conflicting); |
| 270 | + |
| 271 | + // Expected behavior: |
| 272 | + // - Checkpoint at 4 should be displaced with a placeholder containing block_5's prev_blockhash |
| 273 | + // - Checkpoint at 5 should have the inserted data |
| 274 | + // - Checkpoint at 6 should be purged due to prev_blockhash conflict |
| 275 | + |
| 276 | + // Verify height 4 is displaced with placeholder |
| 277 | + let height_4 = result.get(4).expect("checkpoint at 4 should exist"); |
| 278 | + assert_eq!(height_4.height(), 4); |
| 279 | + assert_eq!( |
| 280 | + height_4.hash(), |
| 281 | + block_5_conflicting.prev_blockhash, |
| 282 | + "checkpoint at 4 should be displaced with block 5's prev_blockhash" |
| 283 | + ); |
| 284 | + assert!( |
| 285 | + height_4.data_ref().is_none(), |
| 286 | + "checkpoint at 4 should be a placeholder" |
| 287 | + ); |
| 288 | + |
| 289 | + // Verify height 5 has the inserted data |
| 290 | + let height_5 = result.get(5).expect("checkpoint at 5 should exist"); |
| 291 | + assert_eq!(height_5.height(), 5); |
| 292 | + assert_eq!(height_5.hash(), block_5_conflicting.blockhash); |
| 293 | + assert!( |
| 294 | + height_5.data_ref().is_some(), |
| 295 | + "checkpoint at 5 should have data" |
| 296 | + ); |
| 297 | + |
| 298 | + // Verify height 6 is purged |
| 299 | + assert!( |
| 300 | + result.get(6).is_none(), |
| 301 | + "checkpoint at 6 should be purged due to prev_blockhash conflict" |
| 302 | + ); |
| 303 | + |
| 304 | + // Verify chain structure |
| 305 | + assert_eq!(result.height(), 5, "tip should be at height 5"); |
| 306 | + // Should have: 3 (placeholder), 4 (placeholder), 5 |
| 307 | + assert_eq!(result.iter().count(), 3, "should have 3 checkpoints"); |
| 308 | +} |
| 309 | + |
| 310 | +/// Test that push returns Err(self) when trying to push at the same height. |
| 311 | +#[test] |
| 312 | +fn checkpoint_push_fails_same_height() { |
| 313 | + let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100")); |
| 314 | + |
| 315 | + // Try to push at the same height (100) |
| 316 | + let result = cp.clone().push(100, hash!("another_block_100")); |
| 317 | + |
| 318 | + assert!( |
| 319 | + result.is_err(), |
| 320 | + "push should fail when height is same as current" |
| 321 | + ); |
| 322 | + assert!( |
| 323 | + result.unwrap_err().eq_ptr(&cp), |
| 324 | + "should return self on error" |
| 325 | + ); |
| 326 | +} |
| 327 | + |
| 328 | +/// Test that push returns Err(self) when trying to push at a lower height. |
| 329 | +#[test] |
| 330 | +fn checkpoint_push_fails_lower_height() { |
| 331 | + let cp: CheckPoint<BlockHash> = CheckPoint::new(100, hash!("block_100")); |
| 332 | + |
| 333 | + // Try to push at a lower height (99) |
| 334 | + let result = cp.clone().push(99, hash!("block_99")); |
| 335 | + |
| 336 | + assert!( |
| 337 | + result.is_err(), |
| 338 | + "push should fail when height is lower than current" |
| 339 | + ); |
| 340 | + assert!( |
| 341 | + result.unwrap_err().eq_ptr(&cp), |
| 342 | + "should return self on error" |
| 343 | + ); |
| 344 | +} |
| 345 | + |
| 346 | +/// Test that push returns Err(self) when prev_blockhash conflicts with self's hash. |
| 347 | +#[test] |
| 348 | +fn checkpoint_push_fails_conflicting_prev_blockhash() { |
| 349 | + let cp: CheckPoint<TestBlock> = CheckPoint::new( |
| 350 | + 100, |
| 351 | + TestBlock { |
| 352 | + blockhash: hash!("block_100"), |
| 353 | + prev_blockhash: hash!("block_99"), |
| 354 | + }, |
| 355 | + ); |
| 356 | + |
| 357 | + // Create a block with a prev_blockhash that doesn't match cp's hash |
| 358 | + let conflicting_block = TestBlock { |
| 359 | + blockhash: hash!("block_101"), |
| 360 | + prev_blockhash: hash!("wrong_block_100"), // This conflicts with cp's hash |
| 361 | + }; |
| 362 | + |
| 363 | + // Try to push at height 101 (contiguous) with conflicting prev_blockhash |
| 364 | + let result = cp.clone().push(101, conflicting_block); |
| 365 | + |
| 366 | + assert!( |
| 367 | + result.is_err(), |
| 368 | + "push should fail when prev_blockhash conflicts" |
| 369 | + ); |
| 370 | + assert!( |
| 371 | + result.unwrap_err().eq_ptr(&cp), |
| 372 | + "should return self on error" |
| 373 | + ); |
| 374 | +} |
| 375 | + |
| 376 | +/// Test that push succeeds when prev_blockhash matches self's hash for contiguous height. |
| 377 | +#[test] |
| 378 | +fn checkpoint_push_succeeds_matching_prev_blockhash() { |
| 379 | + let cp: CheckPoint<TestBlock> = CheckPoint::new( |
| 380 | + 100, |
| 381 | + TestBlock { |
| 382 | + blockhash: hash!("block_100"), |
| 383 | + prev_blockhash: hash!("block_99"), |
| 384 | + }, |
| 385 | + ); |
| 386 | + |
| 387 | + // Create a block with matching prev_blockhash |
| 388 | + let matching_block = TestBlock { |
| 389 | + blockhash: hash!("block_101"), |
| 390 | + prev_blockhash: hash!("block_100"), // Matches cp's hash |
| 391 | + }; |
| 392 | + |
| 393 | + // Push at height 101 with matching prev_blockhash |
| 394 | + let result = cp.push(101, matching_block); |
| 395 | + |
| 396 | + assert!( |
| 397 | + result.is_ok(), |
| 398 | + "push should succeed when prev_blockhash matches" |
| 399 | + ); |
| 400 | + let new_cp = result.unwrap(); |
| 401 | + assert_eq!(new_cp.height(), 101); |
| 402 | + assert_eq!(new_cp.hash(), hash!("block_101")); |
| 403 | +} |
| 404 | + |
| 405 | +/// Test that push creates a placeholder for non-contiguous heights with prev_blockhash. |
| 406 | +#[test] |
| 407 | +fn checkpoint_push_creates_placeholder_non_contiguous() { |
| 408 | + let cp: CheckPoint<TestBlock> = CheckPoint::new( |
| 409 | + 100, |
| 410 | + TestBlock { |
| 411 | + blockhash: hash!("block_100"), |
| 412 | + prev_blockhash: hash!("block_99"), |
| 413 | + }, |
| 414 | + ); |
| 415 | + |
| 416 | + // Create a block at non-contiguous height with prev_blockhash |
| 417 | + let block_105 = TestBlock { |
| 418 | + blockhash: hash!("block_105"), |
| 419 | + prev_blockhash: hash!("block_104"), |
| 420 | + }; |
| 421 | + |
| 422 | + // Push at height 105 (non-contiguous) |
| 423 | + let result = cp.push(105, block_105); |
| 424 | + |
| 425 | + assert!( |
| 426 | + result.is_ok(), |
| 427 | + "push should succeed for non-contiguous height" |
| 428 | + ); |
| 429 | + let new_cp = result.unwrap(); |
| 430 | + |
| 431 | + // Verify the tip is at 105 |
| 432 | + assert_eq!(new_cp.height(), 105); |
| 433 | + assert_eq!(new_cp.hash(), hash!("block_105")); |
| 434 | + |
| 435 | + // Verify placeholder was created at 104 |
| 436 | + let placeholder = new_cp.get(104).expect("should have placeholder at 104"); |
| 437 | + assert_eq!(placeholder.hash(), hash!("block_104")); |
| 438 | + assert!( |
| 439 | + placeholder.data_ref().is_none(), |
| 440 | + "placeholder should have no data" |
| 441 | + ); |
| 442 | + |
| 443 | + // Verify chain structure: 100 -> 99 (placeholder) -> 104 (placeholder) -> 105 |
| 444 | + assert_eq!(new_cp.iter().count(), 4); |
| 445 | +} |
0 commit comments