Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::drive::contract::paths::contract_storage_path_vec;
use crate::drive::contract::paths::{contract_root_path_vec, contract_storage_path_vec};
use crate::drive::Drive;
use crate::error::proof::ProofError;
use crate::error::Error;
Expand Down Expand Up @@ -51,6 +51,29 @@ impl Drive {
let (root_hash, mut proved_key_values) =
GroveDb::verify_query(proof, &path_query, &platform_version.drive.grove_version)?;

// A contract that does NOT keep history stores its single current version as an
// Item at the contract root path (key [0]), one level above the history subtree.
// Querying history for such a contract proves THAT single Item instead of the
// (non-existent) history subtree. Treat it as a valid empty history rather than a
// corrupted proof. Require exactly one proved element that is a present item, so any
// other shape at this path still fails as corrupted (mirroring the strict path
// discrimination applied to the history entries below).
if proved_key_values.len() == 1 {
let (path, key, maybe_element) = &proved_key_values[0];
if path == &contract_root_path_vec(&contract_id) && key == &vec![0u8] {
let is_item = maybe_element
.as_ref()
.map(|element| element.is_any_item())
.unwrap_or(false);
if !is_item {
return Err(Error::Proof(ProofError::CorruptedProof(
"expected a contract item at the contract root path".to_string(),
)));
}
return Ok((root_hash, Some(BTreeMap::new())));
}
}
Comment on lines +61 to +75

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Compare key as a slice to avoid the Vec allocation

key == &vec![0u8] allocates a one-byte Vec on every verification just to compare against a constant. Use a slice compare instead — it avoids the allocation and reads more idiomatically. Cold path, so no measurable perf impact, but trivial to tighten. The path == &contract_root_path_vec(&contract_id) allocation is harder to eliminate without a new helper, so leave it.

Suggested change
if proved_key_values.len() == 1 {
let (path, key, maybe_element) = &proved_key_values[0];
if path == &contract_root_path_vec(&contract_id) && key == &vec![0u8] {
let is_item = maybe_element
.as_ref()
.map(|element| element.is_any_item())
.unwrap_or(false);
if !is_item {
return Err(Error::Proof(ProofError::CorruptedProof(
"expected a contract item at the contract root path".to_string(),
)));
}
return Ok((root_hash, Some(BTreeMap::new())));
}
}
if proved_key_values.len() == 1 {
let (path, key, maybe_element) = &proved_key_values[0];
if path == &contract_root_path_vec(&contract_id) && key.as_slice() == [0u8] {
let is_item = maybe_element
.as_ref()
.map(|element| element.is_any_item())
.unwrap_or(false);
if !is_item {
return Err(Error::Proof(ProofError::CorruptedProof(
"expected a contract item at the contract root path".to_string(),
)));
}
return Ok((root_hash, Some(BTreeMap::new())));
}
}

source: ['claude', 'codex']


let mut contracts: BTreeMap<u64, DataContract> = BTreeMap::new();
for (path, key, maybe_element) in proved_key_values.drain(..) {
if path != contract_storage_path_vec(&contract_id) {
Expand Down Expand Up @@ -238,4 +261,99 @@ mod tests {
"should have 2 history entries with limit=2"
);
}

#[test]
fn should_return_empty_history_for_non_history_contract() {
let drive = setup_drive_with_initial_state_structure(None);
let platform_version = PlatformVersion::latest();

let mut contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
.data_contract_owned();
// The contract does NOT keep history (the default, but make it explicit).
contract.config_mut().set_keeps_history(false);
contract.config_mut().set_readonly(false);

let contract_id = contract.id().to_buffer();

// Apply the contract once at time 1000
apply_contract(
&drive,
&contract,
BlockInfo {
time_ms: 1000,
height: 100,
core_height: 10,
epoch: Default::default(),
},
);

// Prove history starting from time 0
let proof = drive
.prove_contract_history(contract_id, None, 0, Some(10), None, platform_version)
.expect("should prove contract history for a non-history contract");

let (_root_hash, verified_history) = Drive::verify_contract_history(
&proof,
contract_id,
0,
Some(10),
None,
platform_version,
)
.expect("should verify contract history for a non-history contract");

let history = verified_history.expect("history should be Some (an empty map)");
assert!(
history.is_empty(),
"a contract that does not keep history should return an empty history map"
);
}

#[test]
fn should_return_empty_history_for_non_history_contract_with_params() {
let drive = setup_drive_with_initial_state_structure(None);
let platform_version = PlatformVersion::latest();

let mut contract = get_data_contract_fixture(None, 0, platform_version.protocol_version)
.data_contract_owned();
contract.config_mut().set_keeps_history(false);
contract.config_mut().set_readonly(false);

let contract_id = contract.id().to_buffer();

apply_contract(
&drive,
&contract,
BlockInfo {
time_ms: 1000,
height: 100,
core_height: 10,
epoch: Default::default(),
},
);

// A non-default limit and start_at_ms must not change the empty result.
// (offset is intentionally not exercised here: count-offset pagination is a
// prove-side limitation that errors against a non-history contract's item path,
// and the FFI never sends a non-zero offset for the no-history case anyway.)
let proof = drive
.prove_contract_history(contract_id, None, 500, Some(5), None, platform_version)
.expect("should prove contract history for a non-history contract");

let (_root_hash, verified_history) = Drive::verify_contract_history(
&proof,
contract_id,
500,
Some(5),
None,
platform_version,
)
.expect("should verify contract history for a non-history contract");

let history = verified_history.expect("history should be Some (an empty map)");
assert!(
history.is_empty(),
"a non-history contract should return empty regardless of limit/start_at_ms"
);
}
}
Loading