diff --git a/Cargo.lock b/Cargo.lock index e7022fb027f..93d6b1b47a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2689,7 +2689,7 @@ dependencies = [ [[package]] name = "grovedb" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "axum 0.8.9", "bincode", @@ -2727,7 +2727,7 @@ dependencies = [ [[package]] name = "grovedb-bulk-append-tree" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "bincode", "blake3", @@ -2743,7 +2743,7 @@ dependencies = [ [[package]] name = "grovedb-commitment-tree" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "blake3", "grovedb-bulk-append-tree", @@ -2759,7 +2759,7 @@ dependencies = [ [[package]] name = "grovedb-costs" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "integer-encoding", "intmap", @@ -2769,7 +2769,7 @@ dependencies = [ [[package]] name = "grovedb-dense-fixed-sized-merkle-tree" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "bincode", "blake3", @@ -2782,7 +2782,7 @@ dependencies = [ [[package]] name = "grovedb-element" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "bincode", "bincode_derive", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "grovedb-epoch-based-storage-flags" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "grovedb-costs", "hex", @@ -2809,7 +2809,7 @@ dependencies = [ [[package]] name = "grovedb-merk" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "bincode", "bincode_derive", @@ -2835,7 +2835,7 @@ dependencies = [ [[package]] name = "grovedb-merkle-mountain-range" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "bincode", "blake3", @@ -2846,7 +2846,7 @@ dependencies = [ [[package]] name = "grovedb-path" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "hex", ] @@ -2854,7 +2854,7 @@ dependencies = [ [[package]] name = "grovedb-query" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "bincode", "byteorder", @@ -2870,7 +2870,7 @@ dependencies = [ [[package]] name = "grovedb-storage" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "blake3", "grovedb-costs", @@ -2889,7 +2889,7 @@ dependencies = [ [[package]] name = "grovedb-version" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "thiserror 2.0.18", "versioned-feature-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2898,7 +2898,7 @@ dependencies = [ [[package]] name = "grovedb-visualize" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "hex", "itertools 0.14.0", @@ -2907,7 +2907,7 @@ dependencies = [ [[package]] name = "grovedbg-types" version = "4.0.0" -source = "git+https://github.com/dashpay/grovedb?rev=a18f7929460ef9c5d814f61ff84d8805b2a1761b#a18f7929460ef9c5d814f61ff84d8805b2a1761b" +source = "git+https://github.com/dashpay/grovedb?rev=40521b831ca0792e4d63f4d8949c45f07517641e#40521b831ca0792e4d63f4d8949c45f07517641e" dependencies = [ "serde", "serde_with 3.20.0", @@ -2945,9 +2945,9 @@ dependencies = [ [[package]] name = "halo2_gadgets" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45824ce0dd12e91ec0c68ebae2a7ed8ae19b70946624c849add59f1d1a62a143" +checksum = "fb2a697cad929f706b7987fe804ad57d43622cd37463ba7e4d662a926fdcfea3" dependencies = [ "arrayvec", "bitvec", @@ -4531,8 +4531,8 @@ dependencies = [ [[package]] name = "orchard" -version = "0.13.1" -source = "git+https://github.com/dashpay/orchard.git?rev=898258d76aab2822249492aede59a02d49278fff#898258d76aab2822249492aede59a02d49278fff" +version = "0.14.0" +source = "git+https://github.com/dashpay/orchard.git?tag=dashified-0.14.0#f05557390a5843bc4eb04c66d8140bc9ef0fe9b7" dependencies = [ "aes", "bitvec", diff --git a/packages/rs-dpp/Cargo.toml b/packages/rs-dpp/Cargo.toml index 6bfb4536a9e..186f4d3f0d6 100644 --- a/packages/rs-dpp/Cargo.toml +++ b/packages/rs-dpp/Cargo.toml @@ -71,7 +71,7 @@ strum = { version = "0.26", features = ["derive"] } json-schema-compatibility-validator = { path = '../rs-json-schema-compatibility-validator', optional = true } once_cell = "1.19.0" tracing = { version = "0.1.41" } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", optional = true } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", optional = true } [dev-dependencies] tokio = { version = "1.40", features = ["full"] } diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index 7059bba18e0..4eb4317a39d 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -82,7 +82,7 @@ derive_more = { version = "1.0", features = ["from", "deref", "deref_mut"] } async-trait = "0.1.77" console-subscriber = { version = "0.4", optional = true } bls-signatures = { git = "https://github.com/dashpay/bls-signatures", rev = "0842b17583888e8f46c252a4ee84cdfd58e0546f", optional = true } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } nonempty = "0.11" # Shielded-pool snapshot needs raw RocksDB SstFileWriter + ingest_external_file_cf # bindings, and blake3 for the snapshot-file checksum. @@ -107,7 +107,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ drive = { path = "../rs-drive", features = ["fixtures-and-mocks"] } drive-proof-verifier = { path = "../rs-drive-proof-verifier" } strategy-tests = { path = "../strategy-tests" } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", features = ["client"] } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", features = ["client"] } assert_matches = "1.5.0" drive-abci = { path = ".", features = ["testing-config", "mocks", "shielded_test_data"] } bls-signatures = { git = "https://github.com/dashpay/bls-signatures", rev = "0842b17583888e8f46c252a4ee84cdfd58e0546f" } @@ -121,8 +121,8 @@ integer-encoding = { version = "4.0.0" } # For dump_only_default_and_aux_cfs_under_shielded_subtree_prefix — same # subtree-prefix algorithm grovedb uses internally. -grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } -grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } +grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } +grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } [features] default = ["bls-signatures"] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs index 3a003d6f308..1a78c9f7854 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs @@ -14,9 +14,9 @@ use drive::fees::op::LowLevelDriveOperation; use drive::grovedb::TransactionArg; use drive::state_transition_action::StateTransitionAction; use grovedb_commitment_tree::{ - redpallas, Action, Anchor, Authorized, BatchValidator, Bundle, DashMemo, - ExtractedNoteCommitment, Flags, NoteBytesData, Nullifier, Proof, TransmittedNoteCiphertext, - ValueCommitment, VerifyingKey, + redpallas, Action, ActionFromPartsError, Anchor, Authorized, BatchValidator, Bundle, DashMemo, + ExtractedNoteCommitment, Flags, NoteBytesData, Nullifier, Proof, ProofSizeEnforcement, + TransmittedNoteCiphertext, ValueCommitment, VerifyingKey, }; use std::sync::OnceLock; @@ -127,9 +127,18 @@ pub fn reconstruct_and_verify_bundle( InvalidShieldedProofError::new("invalid value commitment bytes".to_string()) })?; - // `Action::from_parts` returns `None` when `rk` is the identity key - // (an Orchard hardening added upstream in 0.13). Reject those as - // malformed rather than silently dropping them. + // `Action::from_parts` rejects malformed actions instead of silently + // dropping them. In orchard 0.14 it returns `Result<_, ActionFromPartsError>` + // (was `Option` in 0.13) and now enforces TWO invariants: + // - `IdentityRk`: the randomizer key `rk` must be non-identity (the + // hardening that already existed in 0.13). + // - `InvalidEpk`: the ephemeral public key `epk` must encode a + // non-identity point (a NEW invariant in 0.14 — the circuit + // soundness fix). Rejecting this is REQUIRED to preserve soundness; + // we must not weaken it back to acceptance. + // We keep the original "identity randomizer key" message for the + // `IdentityRk` case (byte-for-byte compatible with the 0.13 error path) + // and surface the new `InvalidEpk` rejection with its own message. let action = Action::from_parts( nullifier, rk, @@ -142,8 +151,17 @@ pub fn reconstruct_and_verify_bundle( cv_net, redpallas::Signature::from(a.spend_auth_sig), ) - .ok_or_else(|| { - InvalidShieldedProofError::new("action has identity randomizer key".to_string()) + .map_err(|e| match e { + ActionFromPartsError::IdentityRk => { + InvalidShieldedProofError::new("action has identity randomizer key".to_string()) + } + ActionFromPartsError::InvalidEpk => InvalidShieldedProofError::new( + "action has invalid ephemeral public key (identity or undecodable epk)".to_string(), + ), + // `ActionFromPartsError` is `#[non_exhaustive]`. Any future + // rejection variant added upstream MUST also be rejected here — + // defaulting to acceptance would weaken consensus soundness. + other => InvalidShieldedProofError::new(format!("malformed orchard action: {other}")), })?; orchard_actions.push(action); } @@ -165,13 +183,22 @@ pub fn reconstruct_and_verify_bundle( let actions_nonempty = nonempty::NonEmpty::from_vec(orchard_actions) .ok_or_else(|| InvalidShieldedProofError::new("bundle has no actions".to_string()))?; - let bundle = Bundle::from_parts( + // Reconstruct the `Bundle` (`try_from_parts` is orchard 0.14's only + // public constructor for it). `ProofSizeEnforcement::Strict` rejects a proof + // whose byte-length is not canonical for the action count — anti-malleability. + // Spend-auth signatures were attached per-action in `Action::from_parts` above, + // so action↔signature pairing is preserved with no separate list to reorder. + let bundle = Bundle::try_from_parts( actions_nonempty, orchard_flags, value_balance, orchard_anchor, authorized, - ); + ProofSizeEnforcement::Strict, + ) + .map_err(|e| { + InvalidShieldedProofError::new(format!("failed to reconstruct authorized bundle: {e}")) + })?; // Compute the platform sighash: SHA-256(domain || bundle_commitment || extra_data). // The bundle commitment is the Orchard BundleCommitment (BLAKE2b-256 per ZIP-244), @@ -427,6 +454,236 @@ mod tests { err.message() ); } + + // ---------------------------------------------------------------- + // `Action::from_parts` rejection paths (orchard 0.14 `map_err` arms) + // + // These pin the two consensus-critical rejections that are surfaced + // ONLY inside the `Action::from_parts(...).map_err(...)` arms of + // `reconstruct_and_verify_bundle`: + // - `ActionFromPartsError::IdentityRk` ("identity randomizer key") + // - `ActionFromPartsError::InvalidEpk` ("invalid ephemeral public key") + // + // The earlier error-path tests above never reach those arms: they fail + // at the encrypted-note-size check, the empty-actions check, the + // `rk`-DECODE step ([2u8;32] is not a valid VK encoding, rejected + // *before* `from_parts`), or the flags check. A future refactor that + // mistakenly mapped `InvalidEpk` to acceptance would slip past CI + // without these tests. The `InvalidEpk` rejection is the orchard 0.14 + // circuit-soundness fix and MUST stay a rejection. + // ---------------------------------------------------------------- + + use grovedb_commitment_tree::{ + redpallas, Anchor, Builder, BundleType, DashMemo, Flags as OrchardFlags, + FullViewingKey, NoteValue, Scope, SpendingKey, + }; + + /// Builds a `SerializedAction` whose `nullifier`, `rk`, `cmx`, `cv_net`, + /// and `encrypted_note` (including a real, non-identity `epk`) are all + /// genuine, canonically-encoded Orchard values — so an action built from + /// it decodes cleanly through nullifier → rk → cmx → cv_net and actually + /// REACHES `Action::from_parts`. The bytes are read off a real + /// (unauthorized) output-only Orchard bundle; this needs NO proving key + /// (we never call `create_proof`), so it is cheap. + /// + /// Tests then mutate exactly one field to exercise a single `from_parts` + /// rejection arm. The function asserts each base field decodes, so if a + /// future orchard encoding change broke the precondition the test would + /// fail LOUDLY here rather than silently passing for the wrong reason. + fn valid_base_serialized_action() -> dpp::shielded::SerializedAction { + let sk = SpendingKey::from_bytes([0u8; 32]).expect("valid spending key"); + let fvk = FullViewingKey::from(&sk); + let recipient = fvk.address_at(0u32, Scope::External); + + let mut builder = Builder::::new( + BundleType::Transactional { + flags: OrchardFlags::SPENDS_DISABLED, + bundle_required: false, + }, + Anchor::empty_tree(), + ); + builder + .add_output(None, recipient, NoteValue::from_raw(5_000), [0u8; 36]) + .expect("add_output"); + + let mut rng = rand::rngs::OsRng; + let (unauthorized, _) = builder + .build::(&mut rng) + .expect("build unauthorized bundle") + .expect("bundle is non-empty"); + + // Read genuine, canonically-encoded fields off the first action. + let action = unauthorized.actions().first(); + let enc = action.encrypted_note(); + let mut encrypted_note = Vec::with_capacity(ENCRYPTED_NOTE_SIZE); + encrypted_note.extend_from_slice(&enc.epk_bytes); + encrypted_note.extend_from_slice(enc.enc_ciphertext.as_ref()); + encrypted_note.extend_from_slice(&enc.out_ciphertext); + + let base = dpp::shielded::SerializedAction { + nullifier: action.nullifier().to_bytes(), + rk: <[u8; 32]>::from(action.rk()), + cmx: action.cmx().to_bytes(), + encrypted_note, + cv_net: action.cv_net().to_bytes(), + spend_auth_sig: [6u8; 64], + }; + + // Precondition guards: confirm the base reaches `from_parts` by + // checking that every field the verifier decodes BEFORE `from_parts` + // is valid, and that the base epk is itself a valid non-identity + // point (so flipping it to the identity is what the InvalidEpk test + // isolates). + assert_eq!(base.encrypted_note.len(), ENCRYPTED_NOTE_SIZE); + assert!( + Option::::from(Nullifier::from_bytes(&base.nullifier)).is_some(), + "base nullifier must decode" + ); + assert!( + redpallas::VerificationKey::::try_from(base.rk).is_ok(), + "base rk must decode as a (non-identity) verification key" + ); + assert!( + Option::::from(ExtractedNoteCommitment::from_bytes( + &base.cmx + )) + .is_some(), + "base cmx must decode" + ); + assert!( + Option::::from(ValueCommitment::from_bytes(&base.cv_net)) + .is_some(), + "base cv_net must decode" + ); + base + } + + /// `Action::from_parts` -> `ActionFromPartsError::IdentityRk`. + /// + /// `rk = [0u8; 32]` is the canonical encoding of the RedPallas identity + /// verification key: it DECODES successfully (so it passes the verifier's + /// pre-`from_parts` rk-decode step, unlike the [2u8;32] decode-failure + /// case in `test_invalid_rk_returns_error`), and `from_parts` then + /// rejects it because the randomizer key is the identity. Pins the + /// `IdentityRk => "identity randomizer key"` arm. + #[test] + fn test_identity_rk_returns_error() { + let mut action = valid_base_serialized_action(); + // Sanity: the identity VK encoding must DECODE (else we'd be + // re-testing the decode-failure path, not the from_parts arm). + assert!( + redpallas::VerificationKey::::try_from([0u8; 32]).is_ok(), + "identity rk [0;32] must decode so it reaches Action::from_parts" + ); + action.rk = [0u8; 32]; // RedPallas identity verification key + + let result = reconstruct_and_verify_bundle( + &[action], + FLAGS_SPENDS_AND_OUTPUTS, + 0, + &[42u8; 32], + &[0u8; 100], + &[0u8; 64], + &[], + ); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.message().contains("identity randomizer key"), + "expected 'identity randomizer key' error from the IdentityRk arm, got: {}", + err.message() + ); + } + + /// `Action::from_parts` -> `ActionFromPartsError::InvalidEpk`. + /// + /// This is the orchard 0.14 circuit-soundness reject. The action is valid + /// everywhere the verifier checks before `from_parts` — including a + /// genuine, non-identity `rk` derived exactly like orchard's own + /// `non_identity_rk()` test helper (the scalar `1` as a RedPallas + /// `SigningKey`, then its `VerificationKey`) — so it passes the rk-decode + /// step AND the `IdentityRk` check, reaching the epk invariant. Its `epk` + /// is then set to `[0u8; 32]`, the canonical Pallas identity encoding, + /// which is NOT a valid `KA^{Orchard}` public key, so `from_parts` + /// rejects with `InvalidEpk`. Pins the + /// `InvalidEpk => "invalid ephemeral public key"` arm. + #[test] + fn test_identity_epk_returns_invalid_epk_error() { + let mut action = valid_base_serialized_action(); + + // Non-identity rk: scalar 1 (little-endian) -> SigningKey -> VK -> bytes. + let mut scalar_one = [0u8; 32]; + scalar_one[0] = 1; + let signing_key = redpallas::SigningKey::::try_from(scalar_one) + .expect("scalar 1 is a valid RedPallas signing key"); + let vk = redpallas::VerificationKey::::from(&signing_key); + let non_identity_rk = <[u8; 32]>::from(vk); + // Guard: this rk must NOT be the identity (else we'd trip IdentityRk + // instead of reaching the epk check). + assert_ne!( + non_identity_rk, [0u8; 32], + "scalar-1 verification key must be non-identity" + ); + action.rk = non_identity_rk; + + // Set the ephemeral public key (first 32 bytes of encrypted_note) to + // the canonical Pallas identity encoding — an invalid epk. + action.encrypted_note[..EPK_SIZE].copy_from_slice(&[0u8; EPK_SIZE]); + + let result = reconstruct_and_verify_bundle( + &[action], + FLAGS_SPENDS_AND_OUTPUTS, + 0, + &[42u8; 32], + &[0u8; 100], + &[0u8; 64], + &[], + ); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.message().contains("invalid ephemeral public key"), + "expected 'invalid ephemeral public key' error from the InvalidEpk arm, got: {}", + err.message() + ); + } + + /// `Bundle::try_from_parts(.., ProofSizeEnforcement::Strict)` -> + /// `BundleError::NonCanonicalProofSize`. + /// + /// Pins the proof-size policy. The base action is valid, so reconstruction + /// clears `Action::from_parts` and reaches `try_from_parts`; the proof + /// byte-length (100) is not canonical for a single-action bundle, so + /// `Strict` rejects it. This is what distinguishes `Strict` from + /// `Unenforced`: under `Unenforced` the bundle would build and this test + /// would fail. The positive round-trip tests use canonical proofs and pass + /// under either setting, so without this test a refactor could silently flip + /// the policy. A 32-byte zero anchor (field element 0) is used so anchor + /// decoding succeeds and we reach the proof-size check. + #[test] + fn test_noncanonical_proof_size_rejected_under_strict() { + let action = valid_base_serialized_action(); + let result = reconstruct_and_verify_bundle( + &[action], + FLAGS_SPENDS_AND_OUTPUTS, + 0, + &[0u8; 32], + &[0u8; 100], // non-canonical proof length for a 1-action bundle + &[0u8; 64], + &[], + ); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + err.message() + .contains("failed to reconstruct authorized bundle"), + "expected NonCanonicalProofSize rejection from try_from_parts(Strict), got: {}", + err.message() + ); + } } // ========================================== diff --git a/packages/rs-drive/Cargo.toml b/packages/rs-drive/Cargo.toml index 48b3d8cfc75..808402e721a 100644 --- a/packages/rs-drive/Cargo.toml +++ b/packages/rs-drive/Cargo.toml @@ -52,12 +52,12 @@ enum-map = { version = "2.0.3", optional = true } intmap = { version = "3.0.1", features = ["serde"], optional = true } chrono = { version = "0.4.35", optional = true } itertools = { version = "0.13", optional = true } -grovedb = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", optional = true, default-features = false } -grovedb-costs = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", optional = true } -grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } -grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", optional = true } -grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } -grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } +grovedb = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", optional = true, default-features = false } +grovedb-costs = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", optional = true } +grovedb-path = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } +grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", optional = true } +grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } +grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } [dev-dependencies] criterion = "0.5" diff --git a/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists/mod.rs b/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists/mod.rs index 0524064598a..3a41b10e98e 100644 --- a/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists/mod.rs +++ b/packages/rs-drive/src/util/grove_operations/grove_insert_if_not_exists/mod.rs @@ -51,3 +51,115 @@ impl Drive { } } } + +#[cfg(test)] +mod v11_consensus_regression_tests { + use crate::util::test_helpers::setup::setup_drive; + use dpp::version::PlatformVersion; + use grovedb::Element; + use grovedb_path::SubtreePath; + + /// Protocol-v11 (`GROVE_V2`) consensus regression guard — testnet block 245,344. + /// + /// This models the v11 AddressBalances tree-set built by `transition_to_version_11`: an + /// `empty_sum_tree` root at `[56]` (the control) plus an `empty_provable_count_sum_tree` + /// (CLEAR_ADDRESS_POOL, `[56,'c']`) under it, both via `grove_insert_if_not_exists`. Under the + /// v11 grove version (`GROVE_V2`) the provable-count-sum tree must be inserted as a plain value + /// (`Op::Put`), NOT a layered subtree (`Op::PutLayeredReference`): the latter folds the child + /// root into the parent node's `value_hash`, changing the grovedb root and breaking consensus on + /// replay (a beta.2 node computed `98DD9B…` instead of the canonical `29B639…` and stalled at + /// block 245,344). + /// + /// grovedb #759 version-gates this dispatch: `GROVE_V1`/`GROVE_V2` keep `Op::Put` + /// (slot v0); `GROVE_V3` (protocol v12) adopts the layered subtree (slot v1), + /// consistent with the batch path. This test pins BOTH sides of the gate so neither + /// can silently flip: v11/`GROVE_V2` → the canonical `Op::Put` roots, and + /// v12/`GROVE_V3` → a *different* `provable_count_sum_tree` root (intentional + /// layered behaviour). `empty_sum_tree` is the unchanged control (layered always). + #[test] + fn provable_count_sum_tree_insert_preserves_v11_consensus_root() { + // grovedb v4.1.0 (`Op::Put`) golden roots — the canonical protocol-v11 chain. + const GOLDEN_1: [u8; 32] = [ + 193, 62, 168, 151, 156, 164, 202, 8, 147, 137, 134, 209, 196, 32, 2, 85, 18, 100, 97, + 227, 62, 160, 254, 196, 250, 171, 84, 176, 58, 38, 16, 116, + ]; + const GOLDEN_2: [u8; 32] = [ + 35, 99, 15, 178, 25, 57, 206, 47, 187, 195, 100, 28, 97, 85, 113, 230, 135, 22, 34, + 126, 72, 125, 158, 90, 116, 94, 214, 136, 96, 195, 235, 46, + ]; + + // Insert empty_sum_tree at [56] (AddressBalances, the control) then + // empty_provable_count_sum_tree at [56,'c'] (CLEAR_ADDRESS_POOL, the regressed + // op) under `pv`, returning the grovedb root after each insert. + fn insert_v11_address_trees(pv: &PlatformVersion) -> ([u8; 32], [u8; 32]) { + let drive = setup_drive(None); + drive + .grove_insert_if_not_exists( + SubtreePath::empty(), + &[56u8], + Element::empty_sum_tree(), + None, + None, + &pv.drive, + ) + .expect("insert sum_tree at [56]"); + let root_1 = drive + .grove + .root_hash(None, &pv.drive.grove_version) + .unwrap() + .unwrap(); + + let pcs_path: Vec> = vec![vec![56u8]]; + drive + .grove_insert_if_not_exists( + pcs_path.as_slice().into(), + b"c", + Element::empty_provable_count_sum_tree(), + None, + None, + &pv.drive, + ) + .expect("insert provable_count_sum_tree at [56,'c']"); + let root_2 = drive + .grove + .root_hash(None, &pv.drive.grove_version) + .unwrap() + .unwrap(); + (root_1, root_2) + } + + // v11 / GROVE_V2 — the consensus-locked path: must be `Op::Put` (golden). + let (v11_root_1, v11_root_2) = + insert_v11_address_trees(PlatformVersion::get(11).expect("protocol v11")); + eprintln!("v11 root_1 (control sum_tree) = {v11_root_1:?}"); + eprintln!("v11 root_2 (provable_count_sum_tree) = {v11_root_2:?}"); + assert_eq!( + v11_root_1, GOLDEN_1, + "v11 control sum_tree root changed unexpectedly" + ); + assert_eq!( + v11_root_2, GOLDEN_2, + "ProvableCountSumTree insert under GROVE_V2 no longer matches grovedb v4.1.0 \ + (Op::Put) — protocol-v11 consensus regression (testnet block 245,344)" + ); + + // v12 / GROVE_V3 — intentionally layered (grovedb #759 version gate): the + // provable_count_sum_tree root MUST differ from the v11 Op::Put golden, while the + // empty_sum_tree control MUST stay on GOLDEN_1 — the gate is scoped to + // CountSumTree / ProvableCount[Sum]Tree only, never plain sum_tree. + let (v12_root_1, v12_root_2) = + insert_v11_address_trees(PlatformVersion::get(12).expect("protocol v12")); + eprintln!("v12 root_1 (control sum_tree) = {v12_root_1:?}"); + eprintln!("v12 root_2 (provable_count_sum_tree) = {v12_root_2:?}"); + assert_eq!( + v12_root_1, GOLDEN_1, + "v12 control sum_tree root changed — grovedb #759's version gate must affect only \ + CountSumTree / ProvableCount[Sum]Tree, never plain empty_sum_tree" + ); + assert_ne!( + v12_root_2, GOLDEN_2, + "GROVE_V3 (protocol v12) must use the layered-subtree dispatch (grovedb #759) — \ + a root matching the v11 Op::Put value means the version gate was lost" + ); + } +} diff --git a/packages/rs-platform-version/Cargo.toml b/packages/rs-platform-version/Cargo.toml index cbcd9702ad8..c5d7ad5d9d5 100644 --- a/packages/rs-platform-version/Cargo.toml +++ b/packages/rs-platform-version/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" thiserror = { version = "2.0.12" } bincode = { version = "=2.0.1" } versioned-feature-core = { git = "https://github.com/dashpay/versioned-feature-core", version = "1.0.0" } -grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b" } +grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e" } [features] mock-versions = [] diff --git a/packages/rs-platform-wallet/Cargo.toml b/packages/rs-platform-wallet/Cargo.toml index e9ea678eb55..1d51abe1506 100644 --- a/packages/rs-platform-wallet/Cargo.toml +++ b/packages/rs-platform-wallet/Cargo.toml @@ -49,7 +49,7 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg", zeroize = "1" # Shielded pool (optional, behind `shielded` feature) -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", optional = true } +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", optional = true } # Direct `rusqlite` access so `FileBackedShieldedStore::open_path` can set # WAL + synchronous=NORMAL pragmas before handing the connection to # `ClientPersistentCommitmentTree`. Version locked to match the rev grovedb diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index 97bdbcb1300..6deab0d0536 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -18,7 +18,7 @@ drive = { path = "../rs-drive", default-features = false, features = [ ] } drive-proof-verifier = { path = "../rs-drive-proof-verifier", default-features = false } -grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a18f7929460ef9c5d814f61ff84d8805b2a1761b", features = [ +grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "40521b831ca0792e4d63f4d8949c45f07517641e", features = [ "client", "sqlite", ], optional = true }