-
Notifications
You must be signed in to change notification settings - Fork 54
test(drive-abci): assert check_tx never mutates committed grovedb state #3844
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
49f3993
06536c1
ce544a8
c5ad3c7
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 |
|---|---|---|
|
|
@@ -3820,4 +3820,298 @@ mod tests { | |
| )) | ||
| )); | ||
| } | ||
|
|
||
| /// CheckTx must NEVER mutate committed GroveDB state — the broad-sweep guard for the | ||
| /// 2026-06-10 devnet paloma chain halt (height 788). | ||
| /// | ||
| /// CheckTx fee estimation runs `validate_fees_of_event(..., transaction: None, ...)` (see | ||
| /// `check_tx_v0` above), so an eager GroveDB write anywhere on the CheckTx path (e.g. inside | ||
| /// a drive-op converter, as the pre-#3823 shielded `InsertNullifiers` arm did with | ||
| /// `store_nullifiers_for_block`) commits straight to disk on every node that validates the | ||
| /// gossiped transition. The on-disk root then diverges from the signed app hash and every | ||
| /// proposer panics with "drive and platform state app hash mismatch" — a chain halt that | ||
| /// restarts cannot heal because the write is durable. This test covers the identity-paid | ||
| /// (`ExecutionEvent::Paid`) estimation arm; the asset-lock arm is covered by | ||
| /// `check_tx_does_not_mutate_committed_state_identity_top_up` below, and the exact type-20 | ||
| /// shape that halted paloma by `check_tx_fee_estimation_does_not_mutate_committed_state` in | ||
| /// `identity_create_from_shielded_pool/tests.rs`. | ||
| #[test] | ||
| fn check_tx_does_not_mutate_committed_state_data_contract_create() { | ||
| use crate::test::helpers::state_mutation_guard::assert_committed_root_hash_unchanged; | ||
|
|
||
| let platform_config = PlatformConfig { | ||
| testing_configs: PlatformTestConfig { | ||
| disable_instant_lock_signature_verification: true, | ||
| ..Default::default() | ||
| }, | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| let platform = TestPlatformBuilder::new() | ||
| .with_config(platform_config) | ||
| .build_with_mock_rpc(); | ||
|
|
||
| let platform_state = platform.state.load(); | ||
| let protocol_version = platform_state.current_protocol_version_in_consensus(); | ||
| let platform_version = PlatformVersion::get(protocol_version).unwrap(); | ||
|
|
||
| let (key, private_key) = IdentityPublicKey::random_ecdsa_critical_level_authentication_key( | ||
| 1, | ||
| Some(1), | ||
| platform_version, | ||
| ) | ||
| .expect("expected to get key pair"); | ||
|
|
||
| platform | ||
| .drive | ||
| .create_initial_state_structure(None, platform_version) | ||
| .expect("expected to create state structure"); | ||
| let identity: Identity = IdentityV0 { | ||
| id: Identifier::new([ | ||
| 158, 113, 180, 126, 91, 83, 62, 44, 83, 54, 97, 88, 240, 215, 84, 139, 167, 156, | ||
| 166, 203, 222, 4, 64, 31, 215, 199, 149, 151, 190, 246, 251, 44, | ||
| ]), | ||
| public_keys: BTreeMap::from([(1, key.clone())]), | ||
| balance: 25_000_000_000, // 0.25 Dash | ||
| revision: 0, | ||
| } | ||
| .into(); | ||
|
|
||
| let dashpay = get_dashpay_contract_fixture(Some(identity.id()), 1, protocol_version); | ||
| let mut create_contract_state_transition: StateTransition = dashpay | ||
| .try_into_platform_versioned(platform_version) | ||
| .expect("expected a state transition"); | ||
| create_contract_state_transition | ||
| .sign(&key, private_key.as_slice(), &NativeBlsModule) | ||
| .expect("expected to sign transition"); | ||
| let serialized = create_contract_state_transition | ||
| .serialize_to_bytes() | ||
| .expect("serialized state transition"); | ||
| platform | ||
| .drive | ||
| .add_new_identity( | ||
| identity, | ||
| false, | ||
| &BlockInfo::default(), | ||
| true, | ||
| None, | ||
| platform_version, | ||
| ) | ||
| .expect("expected to insert identity"); | ||
|
|
||
| let platform_ref = PlatformRef { | ||
| drive: &platform.drive, | ||
| state: &platform_state, | ||
| config: &platform.config, | ||
| core_rpc: &platform.core_rpc, | ||
| }; | ||
|
|
||
| let first_time_result = assert_committed_root_hash_unchanged( | ||
| &platform.drive, | ||
| platform_version, | ||
| "check_tx FirstTimeCheck (data contract create)", | ||
| || { | ||
| platform.check_tx( | ||
| serialized.as_slice(), | ||
| FirstTimeCheck, | ||
| &platform_ref, | ||
| platform_version, | ||
| ) | ||
| }, | ||
| ) | ||
| .expect("expected to check tx"); | ||
| // The fixture must stay a VALID transition: an early consensus rejection would skip the | ||
| // fee-estimation stage and the invariant would be checked against a no-op. | ||
| assert!( | ||
| first_time_result.is_valid(), | ||
| "fixture must remain valid so fee estimation actually runs: {:?}", | ||
| first_time_result.errors | ||
| ); | ||
|
|
||
| let recheck_result = assert_committed_root_hash_unchanged( | ||
| &platform.drive, | ||
| platform_version, | ||
| "check_tx Recheck (data contract create)", | ||
| || { | ||
| platform.check_tx( | ||
| serialized.as_slice(), | ||
| Recheck, | ||
| &platform_ref, | ||
| platform_version, | ||
| ) | ||
| }, | ||
| ) | ||
| .expect("expected to check tx"); | ||
| assert!(recheck_result.is_valid()); | ||
|
Comment on lines
+3931
to
+3945
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. 💬 Nitpick: Both source: ['claude'] |
||
| } | ||
|
|
||
| /// CheckTx for an asset-lock-funded transition (identity top up) must not mutate committed | ||
| /// GroveDB state. Same invariant as | ||
| /// `check_tx_does_not_mutate_committed_state_data_contract_create` (see its docs for the | ||
| /// paloma height-788 halt), exercised through the `PaidFromAssetLock` fee-estimation arm and | ||
| /// the asset-lock Recheck path. | ||
| #[tokio::test] | ||
| async fn check_tx_does_not_mutate_committed_state_identity_top_up() { | ||
| use crate::test::helpers::state_mutation_guard::assert_committed_root_hash_unchanged; | ||
|
|
||
| let platform_config = PlatformConfig { | ||
| testing_configs: PlatformTestConfig { | ||
| disable_instant_lock_signature_verification: true, | ||
| ..Default::default() | ||
| }, | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| let platform = TestPlatformBuilder::new() | ||
| .with_config(platform_config) | ||
| .build_with_mock_rpc(); | ||
|
|
||
| let platform_state = platform.state.load(); | ||
| let platform_version = platform_state.current_platform_version().unwrap(); | ||
|
|
||
| let platform_ref = PlatformRef { | ||
| drive: &platform.drive, | ||
| state: &platform_state, | ||
| config: &platform.config, | ||
| core_rpc: &platform.core_rpc, | ||
| }; | ||
|
|
||
| let mut signer = SimpleSigner::default(); | ||
|
|
||
| let mut rng = StdRng::seed_from_u64(567); | ||
|
|
||
| let (master_key, master_private_key) = | ||
| IdentityPublicKey::random_ecdsa_master_authentication_key(0, Some(3), platform_version) | ||
| .expect("expected to get key pair"); | ||
|
|
||
| signer.add_identity_public_key(master_key.clone(), master_private_key); | ||
|
|
||
| let (key, private_key) = IdentityPublicKey::random_ecdsa_critical_level_authentication_key( | ||
| 1, | ||
| Some(19), | ||
| platform_version, | ||
| ) | ||
| .expect("expected to get key pair"); | ||
|
|
||
| signer.add_identity_public_key(key.clone(), private_key); | ||
|
|
||
| let (_, pk) = ECDSA_SECP256K1 | ||
| .random_public_and_private_key_data(&mut rng, platform_version) | ||
| .unwrap(); | ||
|
|
||
| let asset_lock_proof = instant_asset_lock_proof_fixture( | ||
| Some(PrivateKey::from_byte_array(&pk, Network::Testnet).unwrap()), | ||
| None, | ||
| ); | ||
|
|
||
| let identifier = asset_lock_proof | ||
| .create_identifier() | ||
| .expect("expected an identifier"); | ||
|
|
||
| let identity: Identity = IdentityV0 { | ||
| id: identifier, | ||
| public_keys: BTreeMap::from([(0, master_key.clone()), (1, key.clone())]), | ||
| balance: 1000000000, | ||
| revision: 0, | ||
| } | ||
| .into(); | ||
|
|
||
| let identity_create_transition: StateTransition = | ||
| IdentityCreateTransition::try_from_identity_with_signer_and_private_key( | ||
| &identity, | ||
| asset_lock_proof, | ||
| pk.as_slice(), | ||
| &signer, | ||
| &NativeBlsModule, | ||
| 0, | ||
| platform_version, | ||
| ) | ||
| .await | ||
| .expect("expected an identity create transition"); | ||
|
|
||
| let identity_create_serialized_transition = identity_create_transition | ||
| .serialize_to_bytes() | ||
| .expect("serialized state transition"); | ||
|
|
||
| platform | ||
| .drive | ||
| .create_initial_state_structure(None, platform_version) | ||
| .expect("expected to create state structure"); | ||
|
|
||
| let transaction = platform.drive.grove.start_transaction(); | ||
|
|
||
| let validation_result = platform | ||
| .execute_tx(identity_create_serialized_transition, &transaction) | ||
| .expect("expected to execute identity_create tx"); | ||
| assert!(matches!(validation_result, SuccessfulPaidExecution(..))); | ||
|
|
||
| platform | ||
| .drive | ||
| .grove | ||
| .commit_transaction(transaction) | ||
| .unwrap() | ||
| .expect("expected to commit transaction"); | ||
|
|
||
| let (_, pk) = ECDSA_SECP256K1 | ||
| .random_public_and_private_key_data(&mut rng, platform_version) | ||
| .unwrap(); | ||
|
|
||
| let asset_lock_proof_top_up = instant_asset_lock_proof_fixture( | ||
| Some(PrivateKey::from_byte_array(&pk, Network::Testnet).unwrap()), | ||
| None, | ||
| ); | ||
|
|
||
| let identity_top_up_transition: StateTransition = | ||
| IdentityTopUpTransition::try_from_identity_with_private_key( | ||
| &identity, | ||
| asset_lock_proof_top_up, | ||
| pk.as_slice(), | ||
| 0, | ||
| platform_version, | ||
| None, | ||
| ) | ||
| .expect("expected an identity create transition"); | ||
|
|
||
| let identity_top_up_serialized_transition = identity_top_up_transition | ||
| .serialize_to_bytes() | ||
| .expect("serialized state transition"); | ||
|
|
||
| let first_time_result = assert_committed_root_hash_unchanged( | ||
| &platform.drive, | ||
| platform_version, | ||
| "check_tx FirstTimeCheck (identity top up)", | ||
| || { | ||
| platform.check_tx( | ||
| identity_top_up_serialized_transition.as_slice(), | ||
| FirstTimeCheck, | ||
| &platform_ref, | ||
| platform_version, | ||
| ) | ||
| }, | ||
| ) | ||
| .expect("expected to check tx"); | ||
| // The fixture must stay a VALID transition: an early consensus rejection would skip the | ||
| // fee-estimation stage and the invariant would be checked against a no-op. | ||
| assert!( | ||
| first_time_result.is_valid(), | ||
| "fixture must remain valid so fee estimation actually runs: {:?}", | ||
| first_time_result.errors | ||
| ); | ||
|
|
||
| let recheck_result = assert_committed_root_hash_unchanged( | ||
| &platform.drive, | ||
| platform_version, | ||
| "check_tx Recheck (identity top up)", | ||
| || { | ||
| platform.check_tx( | ||
| identity_top_up_serialized_transition.as_slice(), | ||
| Recheck, | ||
| &platform_ref, | ||
| platform_version, | ||
| ) | ||
| }, | ||
| ) | ||
| .expect("expected to check tx"); | ||
| assert!(recheck_result.is_valid()); | ||
| } | ||
| } | ||
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: Doc comment and PR description overstate the actual variant coverage
The doc comment calls this 'the broad-sweep guard for the 2026-06-10 devnet paloma chain halt' and the PR body lists explicit coverage across data contract create + identity top up + masternode vote + all six address-funded variants + all five shielded variants via an
assert_check_tx_valid_at_all_levelshelper that retrofits canonical valid-fixture tests. The shipped diff includes only three guard sites: this test,check_tx_does_not_mutate_committed_state_identity_top_upimmediately below, andcheck_tx_fee_estimation_does_not_mutate_committed_stateinidentity_create_from_shielded_pool/tests.rs. The other address-funded fee-estimation arms (PaidFromAddressInputs,PaidFromAssetLockToPool), the remaining four shielded transition types, masternode vote, identity credit transfer/withdrawal, and theassert_check_tx_valid_at_all_levelshelper are absent.Either land the missing retrofits (preferably alongside the dispatcher guard from the previous finding) or trim the doc comment and PR body so future investigators do not over-trust a safety net that only covers three transitions.
source: ['claude']