Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 9 additions & 15 deletions packages/rs-dpp/src/data_contract/document_type/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,22 +317,11 @@ impl LazyRegex {
}
}

/// The regex of `regex_str`, compiled now and kept, or the error compiling it returns:
/// for a pattern that did not pass through the schema parser's check, such as one read
/// back from a stored document type.
pub fn try_compiled(regex_str: String) -> Result<Self, regex::Error> {
let regex = Regex::new(&regex_str)?;
Ok(LazyRegex {
regex: OnceLock::from(Some(regex)),
regex_str,
})
}

/// Whether `string` matches the pattern, compiled on first use. A pattern that does not
/// compile matches nothing: consensus never holds one, since registration refuses it and a
/// stored document type that holds one does not read back ([`Self::try_compiled`]), so only
/// a contract that skipped both, parsed without validation by a client, reaches it, and the
/// network refuses what that contract's contested documents would need.
/// compile matches nothing: consensus never holds one, since registration refuses it, so
/// only a contract registration never checked reaches it, one parsed without validation by
/// a client or read back from a forged proof, and the network refuses what that contract's
/// contested documents would need.
pub fn is_match(&self, string: &str) -> bool {
self.regex
.get_or_init(|| Regex::new(&self.regex_str).ok())
Expand All @@ -343,6 +332,11 @@ impl LazyRegex {
pub fn as_str(&self) -> &str {
self.regex_str.as_str()
}

/// Whether the pattern has been compiled yet, which happens on its first match.
pub fn is_compiled(&self) -> bool {
self.regex.get().is_some()
}
Comment thread
QuantumExplorer marked this conversation as resolved.
Resolved
}

// Manual Serialize/Deserialize impls deleted in Phase D step 11.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//! terminal, the skip set, the indexed array, and a derived property's resolved type.

use super::property::StoredPropertyType;
use super::stored_error;
use crate::data_contract::document_type::{
ContestedIndexFieldMatch, ContestedIndexInformation, ContestedIndexResolution,
DerivedIndexField, DerivedIndexProperty, DocumentReferenceKind, Index, IndexCountability,
Expand Down Expand Up @@ -49,8 +48,8 @@ pub(super) struct StoredContestedIndex {
resolution: StoredContestedResolution,
}

/// [`ContestedIndexFieldMatch`]: a regex as its pattern, compiled when the index is read back,
/// which refuses a pattern that does not compile.
/// [`ContestedIndexFieldMatch`]: a regex as its pattern, read back uncompiled, as the schema
/// parser keeps it, and compiled on its first match.
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
pub(super) enum StoredContestedFieldMatch {
Regex(String),
Expand Down Expand Up @@ -250,12 +249,12 @@ impl From<&Index> for StoredIndex {
}
}

/// Refuses a contested field's regex that does not compile, as the schema parser does: a stored
/// form can come from anyone through the untrusted decoder.
impl TryFrom<StoredIndex> for Index {
type Error = ProtocolError;

fn try_from(stored: StoredIndex) -> Result<Self, Self::Error> {
/// A contested field's regex is read back as its pattern, compiled on its first match as the
/// schema parser's is, not here: a compiled regex can take megabytes, and a proof's contract is
/// read back before the proof's signature is checked. Registration refuses a pattern that does
/// not compile, so none is stored; one in a forged proof matches nothing.
impl From<StoredIndex> for Index {
fn from(stored: StoredIndex) -> Self {
let StoredIndex {
name,
properties,
Expand All @@ -282,44 +281,32 @@ impl TryFrom<StoredIndex> for Index {
summable_off_count_index,
indexed_array,
} = stored;
let contested_index = contested_index
.map(|contested| {
let field_matches = contested
.field_matches
.into_iter()
.map(|(field, field_match)| {
let field_match = match field_match {
StoredContestedFieldMatch::Regex(pattern) => {
ContestedIndexFieldMatch::Regex(
LazyRegex::try_compiled(pattern).map_err(|error| {
stored_error(format!(
"index {name} has a contested regex on {field} that \
does not compile: {error}"
))
})?,
)
}
StoredContestedFieldMatch::PositiveIntegerMatch(value) => {
ContestedIndexFieldMatch::PositiveIntegerMatch(value)
}
};
Ok((field, field_match))
})
.collect::<Result<_, ProtocolError>>()?;
Ok::<_, ProtocolError>(ContestedIndexInformation {
field_matches,
resolution: match contested.resolution {
StoredContestedResolution::MasternodeVote => {
ContestedIndexResolution::MasternodeVote
let contested_index = contested_index.map(|contested| ContestedIndexInformation {
field_matches: contested
.field_matches
.into_iter()
.map(|(field, field_match)| {
let field_match = match field_match {
StoredContestedFieldMatch::Regex(pattern) => {
ContestedIndexFieldMatch::Regex(LazyRegex::new(pattern))
}
StoredContestedResolution::MasternodeVoteNoLocking => {
ContestedIndexResolution::MasternodeVoteNoLocking
StoredContestedFieldMatch::PositiveIntegerMatch(value) => {
ContestedIndexFieldMatch::PositiveIntegerMatch(value)
}
},
};
(field, field_match)
})
})
.transpose()?;
Ok(Index {
.collect(),
resolution: match contested.resolution {
StoredContestedResolution::MasternodeVote => {
ContestedIndexResolution::MasternodeVote
}
StoredContestedResolution::MasternodeVoteNoLocking => {
ContestedIndexResolution::MasternodeVoteNoLocking
}
},
});
Index {
name,
properties: properties
.into_iter()
Expand Down Expand Up @@ -374,7 +361,7 @@ impl TryFrom<StoredIndex> for Index {
skip_if_absent_properties,
summable_off_count_index,
indexed_array,
})
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,8 @@ impl DocumentTypeInSerializationFormatV0 {
let flattened_properties = properties_from_stored(flattened_properties)?;
let indices: BTreeMap<String, Index> = indices
.into_iter()
.map(|(name, index)| Ok((name, Index::try_from(index)?)))
.collect::<Result<_, ProtocolError>>()?;
.map(|(name, index)| (name, Index::from(index)))
.collect();
let mut index_structure = IndexLevel::try_from_indices_v0(indices.values(), name)?;
// The levels named after a typed array fan the document out over its elements, as the
// parse stamped them when the type was stored
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,42 @@ fn should_refuse_a_stored_rule_nested_deeper_than_the_rule_grammar_admits() {
);
}

/// A contested field's regex is compiled when its index is read back: a stored form that holds
/// one that does not compile is refused, where the lazy regex would have panicked on first
/// use. The schema parser refuses such a pattern at registration.
/// A contested field's regex is read back uncompiled, as the schema parser keeps it, and
/// compiles on its first match: reading a stored type compiles none of its patterns. A pattern
/// that does not compile, which registration refuses so state never holds, reads back and
/// matches nothing.
#[test]
fn should_refuse_a_stored_contested_regex_that_does_not_compile() {
fn should_read_back_a_stored_contested_regex_without_compiling_it() {
let contract =
get_dpns_data_contract_fixture(None, 0, PlatformVersion::latest().protocol_version)
.data_contract_owned();
let (name, index) = contract
let index = contract
.document_types()
.values()
.flat_map(|document_type| document_type.indexes().iter())
.find(|(_, index)| index.contested_index.is_some())
.map(|(name, index)| (name.clone(), index.clone()))
.flat_map(|document_type| document_type.indexes().values())
.find(|index| index.contested_index.is_some())
.cloned()
.expect("expected DPNS to have a contested index");
let regex = |index: &Index| {
index
.contested_index
.as_ref()
.expect("expected the contested index")
.field_matches
.values()
.find_map(|field_match| match field_match {
ContestedIndexFieldMatch::Regex(regex) => Some(regex.clone()),
ContestedIndexFieldMatch::PositiveIntegerMatch(_) => None,
})
.expect("expected a contested regex")
};

let read_back = Index::try_from(StoredIndex::from(&index)).expect("expected the index back");
let read_back = Index::from(StoredIndex::from(&index));
assert_eq!(read_back, index);
let read_back_regex = regex(&read_back);
assert!(!read_back_regex.is_compiled());
assert!(read_back_regex.is_match("dash"));
assert!(read_back_regex.is_compiled());

let mut broken = index;
let contested = broken
Expand All @@ -84,12 +102,8 @@ fn should_refuse_a_stored_contested_regex_that_does_not_compile() {
field,
ContestedIndexFieldMatch::Regex(LazyRegex::new("(".to_string())),
);
let error = Index::try_from(StoredIndex::from(&broken))
.expect_err("expected the stored regex to be refused");
assert!(
error.to_string().contains("does not compile"),
"{name}: {error}"
);
let broken_regex = regex(&Index::from(StoredIndex::from(&broken)));
assert!(!broken_regex.is_match("("));
}

/// A type indexing a typed array reads back with the index structure its parse built: the
Expand Down
86 changes: 72 additions & 14 deletions packages/rs-drive/src/verify/bounded_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,25 @@ pub(super) fn decode_proof_data_contract(
serialized_contract: &[u8],
platform_version: &PlatformVersion,
) -> Result<DataContract, Error> {
// The protocol caps a contract's bytes in the format state transitions carry, and, where
// the verifier's protocol version stores contracts compiled, a compiled contract's bytes
// under their own, larger cap
// Only a protocol version that stores contracts compiled has one in state: before it, an
// item in the compiled form is refused unread
let contract_versions = &platform_version.dpp.contract_versions;
let max_size = if contract_versions.contract_storage_serialization_version
>= u16::from(COMPILED_CONTRACT_FORMAT_TAG)
&& serialized_contract.first() == Some(&COMPILED_CONTRACT_FORMAT_TAG)
let tagged_compiled = serialized_contract.first() == Some(&COMPILED_CONTRACT_FORMAT_TAG);
if tagged_compiled
&& contract_versions.contract_storage_serialization_version
< u16::from(COMPILED_CONTRACT_FORMAT_TAG)
{
return Err(ProtocolError::PlatformDeserializationError(format!(
"proof data contract is in the compiled format, which protocol version {} does not \
store",
platform_version.protocol_version
))
.into());
}

// The protocol caps a contract's bytes in the format state transitions carry, and a
// compiled contract's bytes under their own, larger cap
let max_size = if tagged_compiled {
platform_version
.system_limits
.max_compiled_contract_size
Expand Down Expand Up @@ -130,9 +141,12 @@ fn decode_bounded_vote_reference<T: bincode::de::DecodeUntrusted<()>>(
#[cfg(test)]
mod tests {
use super::*;
use dpp::data_contract::accessors::v0::DataContractV0Getters;
use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
use dpp::data_contract::document_type::{ContestedIndexFieldMatch, LazyRegex};
use dpp::platform_serialization::PLATFORM_BINCODE_CONFIG;
use dpp::serialization::PlatformSerializableWithPlatformVersion;
use dpp::tests::fixtures::get_data_contract_fixture;
use dpp::tests::fixtures::{get_data_contract_fixture, get_dpns_data_contract_fixture};

#[test]
fn rejects_oversized_nested_vote_reference_lengths_without_panicking() {
Expand Down Expand Up @@ -227,16 +241,60 @@ mod tests {
)));
}

/// Before protocol version 15 no contract is stored compiled, so a proof's item in that form
/// is refused before any of it is decoded, whatever its size.
#[test]
fn should_cap_a_proof_contract_tagged_compiled_at_the_wire_size_before_protocol_version_15() {
fn should_refuse_a_proof_contract_stored_compiled_before_protocol_version_15() {
let platform_version = PlatformVersion::get(14).expect("expected protocol version 14");
let over_the_wire_cap =
platform_version.dpp.contract_versions.max_serialized_size as usize + 1;
let contract =
get_dpns_data_contract_fixture(None, 0, PlatformVersion::latest().protocol_version)
.data_contract_owned();
let compiled = contract
.serialize_to_storage_bytes(PlatformVersion::latest())
.expect("serialize the contract for storage");
assert_eq!(compiled[0], COMPILED_CONTRACT_FORMAT_TAG);

for bytes in [compiled, compiled_contract_bytes(1)] {
let error = decode_proof_data_contract(&bytes, platform_version)
.expect_err("expected the compiled form to be refused");
assert!(error.to_string().contains("compiled format"), "{error}");
}
}

assert!(refused_for_size(decode_proof_data_contract(
&compiled_contract_bytes(over_the_wire_cap),
platform_version,
)));
/// A contract's contested regexes are read back from a proof uncompiled, so decoding a forged
/// item, which happens before the proof's signature is checked, compiles none of them; each
/// compiles on its first match.
#[test]
fn should_decode_a_proof_contract_without_compiling_its_contested_regexes() {
let platform_version = PlatformVersion::latest();
let contract = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version)
.data_contract_owned();
let bytes = contract
.serialize_to_storage_bytes(platform_version)
.expect("serialize the contract for storage");
assert_eq!(bytes[0], COMPILED_CONTRACT_FORMAT_TAG);

let decoded =
decode_proof_data_contract(&bytes, platform_version).expect("decode the contract");
let regexes: Vec<&LazyRegex> = decoded
.document_types()
.values()
.flat_map(|document_type| document_type.indexes().values())
.filter_map(|index| index.contested_index.as_ref())
.flat_map(|contested| contested.field_matches.values())
.filter_map(|field_match| match field_match {
ContestedIndexFieldMatch::Regex(regex) => Some(regex),
ContestedIndexFieldMatch::PositiveIntegerMatch(_) => None,
})
.collect();
assert!(
!regexes.is_empty(),
"expected DPNS to have a contested regex"
);
assert!(regexes.iter().all(|regex| !regex.is_compiled()));

assert!(regexes[0].is_match("dash"));
assert!(regexes[0].is_compiled());
}

#[test]
Expand Down
Loading