-
Notifications
You must be signed in to change notification settings - Fork 59
perf(drive-abci): stop rewriting the whole platform state every block #4571
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
base: v4.2-dev
Are you sure you want to change the base?
Changes from all commits
0c13045
ac57c47
ab5087b
0917b56
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 |
|---|---|---|
|
|
@@ -54,6 +54,11 @@ where | |
| // Persist block state | ||
| self.store_platform_state(&block_platform_state, Some(transaction), platform_version)?; | ||
|
|
||
| // Whatever the store wrote is now what is on disk for this block, so the | ||
| // next block only has to write the full record if it changes something | ||
| // heavy itself. | ||
| block_platform_state.heavy_fields_dirty = false; | ||
|
|
||
|
Comment on lines
+57
to
+61
Contributor
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. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Keep
🤖 Prompt for AI Agents |
||
| let block_platform_state = Arc::new(block_platform_state); | ||
|
|
||
| self.state.store(block_platform_state); | ||
|
|
@@ -222,4 +227,129 @@ mod tests { | |
| "genesis_block_info must always be cleared" | ||
| ); | ||
| } | ||
|
|
||
| /// While replaying history the full saved record is rewritten only when a | ||
| /// heavy field changed; the small record carries the block info in between. | ||
| /// A node restarted from disk must see the newest block info and the heavy | ||
| /// fields from the last full write. | ||
| #[test] | ||
| fn v0_historical_block_with_clean_heavy_fields_reloads_from_the_small_record() { | ||
| use crate::config::{PlatformConfig, PlatformTestConfig}; | ||
| use crate::platform_types::platform::Platform; | ||
| use crate::platform_types::platform_state::PlatformState; | ||
| use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0Getters; | ||
| use dpp::dashcore::{ProTxHash, Txid}; | ||
| use dpp::dashcore_rpc::dashcore_rpc_json::{DMNState, MasternodeListItem, MasternodeType}; | ||
| use dpp::serialization::PlatformDeserializableFromVersionedStructure; | ||
|
|
||
| let platform_version = PlatformVersion::latest(); | ||
| let platform = TestPlatformBuilder::new() | ||
| .with_config(PlatformConfig { | ||
| testing_configs: PlatformTestConfig { | ||
| store_platform_state: true, | ||
| ..PlatformTestConfig::default_minimal_verifications() | ||
| }, | ||
| ..Default::default() | ||
| }) | ||
| .build_with_mock_rpc() | ||
| .set_genesis_state(); | ||
|
|
||
| let loaded = platform.state.load(); | ||
| let mut block_platform_state = loaded.as_ref().clone(); | ||
| drop(loaded); | ||
|
|
||
| // A block from long ago, so the store treats it as replayed history. | ||
| let mut old_block = make_extended_block_info(7); | ||
| old_block.basic_info_mut().time_ms = 1_000_000; | ||
|
|
||
| // Block 7 changes a heavy field, so it is written in full. | ||
| let pro_tx_hash = ProTxHash::from_byte_array([0x77u8; 32]); | ||
| let masternode = MasternodeListItem { | ||
| node_type: MasternodeType::Regular, | ||
| pro_tx_hash, | ||
| collateral_hash: Txid::from_byte_array([0u8; 32]), | ||
| collateral_index: 0, | ||
| collateral_address: [0u8; 20], | ||
| operator_reward: 0.0, | ||
| state: DMNState { | ||
| service: "1.2.3.4:1234".parse().expect("socket address"), | ||
| registered_height: 0, | ||
| pose_revived_height: None, | ||
| pose_ban_height: None, | ||
| revocation_reason: 0, | ||
| owner_address: [0u8; 20], | ||
| voting_address: [0u8; 20], | ||
| payout_address: [0u8; 20], | ||
| pub_key_operator: vec![0u8; 48], | ||
| operator_payout_address: None, | ||
| platform_node_id: None, | ||
| platform_p2p_port: None, | ||
| platform_http_port: None, | ||
| }, | ||
| }; | ||
| block_platform_state | ||
| .full_masternode_list_mut() | ||
| .insert(pro_tx_hash, masternode); | ||
| assert!(block_platform_state.heavy_fields_dirty); | ||
|
Comment on lines
+290
to
+293
Collaborator
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. 🟡 Suggestion: Exercise a clean-to-dirty transition in the persistence test The newly constructed platform state already has heavy_fields_dirty = true, and set_genesis_state does not clear it. The assertion after full_masternode_list_mut() therefore does not verify that this accessor marks the state dirty: removing its dirty-bit assignment would still allow block 7 to write in full and block 8 to exercise the small-record merge. Add a third historical block that mutates the heavy collection after block 8 has left the state clean, then verify that the full record advances and the mutation survives reload. This preserves the existing merge coverage while also testing the dirty transition that prevents lost updates. source: ['claude'] |
||
|
|
||
| let transaction = platform.drive.grove.start_transaction(); | ||
| platform | ||
| .update_state_cache_v0( | ||
| old_block, | ||
| block_platform_state, | ||
| &transaction, | ||
| platform_version, | ||
| ) | ||
| .expect("block 7 must be stored"); | ||
|
|
||
| // Block 8 changes nothing heavy, so only the small record is written. | ||
| let loaded = platform.state.load(); | ||
| let block_platform_state = loaded.as_ref().clone(); | ||
| drop(loaded); | ||
| assert!(!block_platform_state.heavy_fields_dirty); | ||
|
|
||
| let mut old_block = make_extended_block_info(8); | ||
| old_block.basic_info_mut().time_ms = 1_000_001; | ||
| platform | ||
| .update_state_cache_v0( | ||
| old_block, | ||
| block_platform_state, | ||
| &transaction, | ||
| platform_version, | ||
| ) | ||
| .expect("block 8 must be stored"); | ||
|
|
||
| let reloaded = Platform::<crate::rpc::core::MockCoreRPCLike>::fetch_platform_state( | ||
| &platform.drive, | ||
| Some(&transaction), | ||
| platform_version, | ||
| ) | ||
| .expect("fetch must succeed") | ||
| .expect("a state was stored"); | ||
|
|
||
| assert_eq!( | ||
| reloaded.last_committed_block_height(), | ||
| 8, | ||
| "block info comes from the small record written at block 8" | ||
| ); | ||
| assert!( | ||
| reloaded.full_masternode_list().contains_key(&pro_tx_hash), | ||
| "heavy fields come from the full record written at block 7" | ||
| ); | ||
|
|
||
| // The full record on disk must still be block 7's: that is what proves | ||
| // block 8 skipped it rather than rewriting it with the same contents. | ||
| let full_bytes = platform | ||
| .drive | ||
| .fetch_platform_state_bytes(Some(&transaction), platform_version) | ||
| .expect("fetch must succeed") | ||
| .expect("a full record was stored"); | ||
| let full_record = PlatformState::versioned_deserialize(&full_bytes, platform_version) | ||
| .expect("full record must deserialize"); | ||
| assert_eq!( | ||
| full_record.last_committed_block_height(), | ||
| 7, | ||
| "the full record is not rewritten for a historical block that changed nothing heavy" | ||
| ); | ||
| } | ||
| } | ||
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.
🟡 Suggestion: Enable checkpoints in the historical-block regression test
The default test configuration sets disable_checkpoints to true, and the crate's dev-dependency enables testing-config. This fixture therefore returns Ok(None) at the initial configuration guard, before reaching the historical-block predicate. Removing the replay-skip check would leave this test passing. Explicitly enable checkpoints, as the neighboring first-block test does, so this assertion exercises the new behavior.
source: ['claude']