diff --git a/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs b/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs index 2432a080c67..c2ecc99126b 100644 --- a/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs +++ b/packages/rs-dpp/src/data_contract/created_data_contract/mod.rs @@ -232,3 +232,196 @@ impl CreatedDataContractInSerializationFormat { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + use crate::tests::fixtures::get_data_contract_fixture; + use crate::version::PlatformVersion; + + fn sample_created() -> CreatedDataContract { + get_data_contract_fixture(None, 42, 1) + } + + // ----------------------------------------------------------------------- + // Constructor / getter coverage + // ----------------------------------------------------------------------- + + #[test] + fn from_contract_and_identity_nonce_wraps_v0() { + let platform_version = PlatformVersion::latest(); + let created = sample_created(); + let contract = created.data_contract().clone(); + let wrapped = CreatedDataContract::from_contract_and_identity_nonce( + contract.clone(), + 99, + platform_version, + ) + .expect("should wrap successfully on latest version"); + assert_eq!(wrapped.identity_nonce(), 99); + assert_eq!(wrapped.data_contract().id(), contract.id()); + // from_contract_and_identity_nonce always produces the V0 variant today. + assert!(matches!(wrapped, CreatedDataContract::V0(_))); + } + + #[test] + fn identity_nonce_getter_returns_underlying_value() { + let created = sample_created(); + assert_eq!(created.identity_nonce(), 42); + } + + #[test] + fn data_contract_getter_returns_same_id() { + let created = sample_created(); + let via_getter = created.data_contract(); + let expected_id = via_getter.id(); + // Also verify the owned getter yields the same id. + let owned = created.clone().data_contract_owned(); + assert_eq!(owned.id(), expected_id); + } + + #[test] + fn data_contract_mut_allows_mutation() { + let mut created = sample_created(); + let original_id = created.data_contract().id(); + let new_id = platform_value::Identifier::from([7u8; 32]); + // Mutate via the mutable accessor to prove it hands out a real mut ref. + created.data_contract_mut().set_id(new_id); + assert_ne!(created.data_contract().id(), original_id); + assert_eq!(created.data_contract().id(), new_id); + } + + #[test] + fn data_contract_and_identity_nonce_extracts_both() { + let created = sample_created(); + let (contract, nonce) = created.clone().data_contract_and_identity_nonce(); + assert_eq!(nonce, 42); + assert_eq!(contract.id(), created.data_contract().id()); + } + + #[test] + fn set_identity_nonce_updates_value() { + let mut created = sample_created(); + created.set_identity_nonce(777); + assert_eq!(created.identity_nonce(), 777); + } + + // ----------------------------------------------------------------------- + // From for DataContract + // ----------------------------------------------------------------------- + + #[test] + fn from_created_to_data_contract() { + let created = sample_created(); + let expected_id = created.data_contract().id(); + let dc: DataContract = created.into(); + assert_eq!(dc.id(), expected_id); + } + + // ----------------------------------------------------------------------- + // Bincode serialize / deserialize round-trip via + // PlatformSerializableWithPlatformVersion + + // PlatformDeserializableWithPotentialValidationFromVersionedStructure. + // ----------------------------------------------------------------------- + + #[test] + fn serialize_roundtrip_via_platform_version() { + let platform_version = PlatformVersion::latest(); + let created = sample_created(); + let bytes = created + .serialize_to_bytes_with_platform_version(platform_version) + .expect("serialize should succeed"); + assert!(!bytes.is_empty()); + + let restored = CreatedDataContract::versioned_deserialize(&bytes, false, platform_version) + .expect("deserialize should succeed"); + assert_eq!(restored.identity_nonce(), created.identity_nonce()); + assert_eq!(restored.data_contract().id(), created.data_contract().id()); + } + + #[test] + fn serialize_consume_to_bytes_matches_clone_path() { + let platform_version = PlatformVersion::latest(); + let created = sample_created(); + let via_ref = created + .serialize_to_bytes_with_platform_version(platform_version) + .expect("ref serialize should succeed"); + let via_consume = created + .clone() + .serialize_consume_to_bytes_with_platform_version(platform_version) + .expect("consume serialize should succeed"); + // The ref path internally clones and delegates to the consume path; + // both must produce byte-identical output. + assert_eq!(via_ref, via_consume); + } + + #[test] + fn versioned_deserialize_rejects_garbage() { + let platform_version = PlatformVersion::latest(); + let garbage = vec![0xFFu8; 16]; + let err = CreatedDataContract::versioned_deserialize(&garbage, false, platform_version) + .expect_err("random bytes should not deserialize"); + match err { + ProtocolError::PlatformDeserializationError(_) => {} + other => panic!("expected PlatformDeserializationError, got {other:?}"), + } + } + + #[test] + fn versioned_deserialize_rejects_empty_input() { + let platform_version = PlatformVersion::latest(); + let err = CreatedDataContract::versioned_deserialize(&[], false, platform_version) + .expect_err("empty input should not deserialize"); + assert!(matches!( + err, + ProtocolError::PlatformDeserializationError(_) + )); + } + + // ----------------------------------------------------------------------- + // CreatedDataContractInSerializationFormat helpers + // ----------------------------------------------------------------------- + + #[test] + fn in_serialization_format_data_contract_and_identity_nonce_owned() { + let platform_version = PlatformVersion::latest(); + let created = sample_created(); + // Re-serialize and re-decode to obtain a raw in-serialization-format value + // without reaching into private fields. + let bytes = created + .clone() + .serialize_consume_to_bytes_with_platform_version(platform_version) + .expect("serialize"); + let config = bincode::config::standard() + .with_big_endian() + .with_no_limit(); + let (decoded, _consumed) = bincode::borrow_decode_from_slice::< + CreatedDataContractInSerializationFormat, + _, + >(&bytes, config) + .expect("raw bincode decode should succeed"); + let (_contract_fmt, nonce) = decoded.data_contract_and_identity_nonce_owned(); + assert_eq!(nonce, created.identity_nonce()); + } + + // ----------------------------------------------------------------------- + // Clone / PartialEq smoke — these derives are real (not generated per + // variant) and are used elsewhere in the codebase via matches on equality. + // ----------------------------------------------------------------------- + + #[test] + fn clone_and_equality() { + let a = sample_created(); + let b = a.clone(); + assert_eq!(a, b); + } + + #[test] + fn different_nonce_breaks_equality() { + let a = sample_created(); + let mut b = a.clone(); + b.set_identity_nonce(a.identity_nonce().wrapping_add(1)); + assert_ne!(a, b); + } +} diff --git a/packages/rs-dpp/src/document/specialized_document_factory/v0/mod.rs b/packages/rs-dpp/src/document/specialized_document_factory/v0/mod.rs index 820e5589a2b..271ee6ece76 100644 --- a/packages/rs-dpp/src/document/specialized_document_factory/v0/mod.rs +++ b/packages/rs-dpp/src/document/specialized_document_factory/v0/mod.rs @@ -721,4 +721,465 @@ mod tests { let ids: Vec<&Identifier> = vec![]; assert!(SpecializedDocumentFactoryV0::is_ownership_the_same(ids)); } + + // ----- Extended coverage ----- + + #[test] + fn new_with_invalid_protocol_version_still_constructs() { + // `new` does not validate version; errors surface only during creation. + let platform_version = PlatformVersion::latest(); + let created = get_data_contract_fixture(None, 0, platform_version.protocol_version); + let factory = SpecializedDocumentFactoryV0::new(u32::MAX, created.data_contract_owned()); + assert_eq!(factory.protocol_version, u32::MAX); + } + + #[test] + fn create_document_bad_protocol_version_returns_error() { + let platform_version = PlatformVersion::latest(); + let created = get_data_contract_fixture(None, 0, platform_version.protocol_version); + let data_contract = created.data_contract_owned(); + let factory = SpecializedDocumentFactoryV0::new_with_entropy_generator( + u32::MAX, + data_contract.clone(), + Box::new(TestEntropyGenerator), + ); + + let result = factory.create_document( + &data_contract, + Identifier::from([1u8; 32]), + 0, + 0, + "noTimeDocument".to_string(), + Value::Null, + ); + assert!(result.is_err()); + } + + #[test] + fn create_document_without_time_bad_protocol_version_returns_error() { + let platform_version = PlatformVersion::latest(); + let created = get_data_contract_fixture(None, 0, platform_version.protocol_version); + let factory = SpecializedDocumentFactoryV0::new_with_entropy_generator( + u32::MAX, + created.data_contract_owned(), + Box::new(TestEntropyGenerator), + ); + + let result = factory.create_document_without_time_based_properties( + Identifier::from([1u8; 32]), + "noTimeDocument".to_string(), + Value::Null, + ); + assert!(result.is_err()); + } + + #[test] + fn create_document_uses_entropy_generator_for_deterministic_id() { + // Two documents for the same type with the same owner+entropy should get identical IDs. + let (factory, _) = setup_factory(); + let owner_id = Identifier::from([42u8; 32]); + + let d1 = factory + .create_document_without_time_based_properties( + owner_id, + "noTimeDocument".to_string(), + platform_value!({ "name": "a" }), + ) + .unwrap(); + let d2 = factory + .create_document_without_time_based_properties( + owner_id, + "noTimeDocument".to_string(), + platform_value!({ "name": "b" }), + ) + .unwrap(); + + // Identifier is derived from (contract_id, owner_id, type_name, entropy); all match → same. + assert_eq!(d1.id(), d2.id()); + } + + #[test] + fn create_document_uses_given_time_based_properties() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([50u8; 32]); + let block_time = 12345u64; + let core_block_height = 67u32; + + // indexedDocument requires createdAt/updatedAt to be set. Provide explicit values. + let data = platform_value!({ + "firstName": "Alice", + "lastName": "Liddell", + }); + + let doc = factory + .create_document( + &data_contract, + owner_id, + block_time, + core_block_height, + "indexedDocument".to_string(), + data, + ) + .unwrap(); + + assert_eq!(doc.owner_id(), owner_id); + // the doc should at least have a non-empty id + assert_ne!(doc.id().as_slice(), &[0u8; 32][..]); + } + + // ----- State transition tests ----- + + #[cfg(feature = "state-transitions")] + mod state_transition_tests { + use super::*; + use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; + use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType; + use crate::state_transition::StateTransitionOwned; + + fn build_document( + factory: &SpecializedDocumentFactoryV0, + owner: Identifier, + type_name: &str, + ) -> Document { + factory + .create_document_without_time_based_properties( + owner, + type_name.to_string(), + platform_value!({ "name": "foo" }), + ) + .expect("document should be created") + } + + #[test] + fn create_state_transition_create_action_populates_owner_and_nonce() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([1u8; 32]); + let doc = build_document(&factory, owner_id, "noTimeDocument"); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + + let mut nonce_counter: BTreeMap<(Identifier, Identifier), u64> = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Create, + vec![(doc, doc_type, Bytes32::new([2u8; 32]), None)], + )]; + + let batch = factory + .create_state_transition(entries, &mut nonce_counter) + .expect("batch transition should be created"); + + assert_eq!(batch.owner_id(), owner_id); + assert_eq!(batch.transitions_len(), 1); + // nonce started at 0 and incremented to 1 + let key = (owner_id, data_contract.id()); + assert_eq!(*nonce_counter.get(&key).unwrap(), 1); + } + + #[test] + fn create_state_transition_no_documents_returns_error() { + let (factory, _) = setup_factory(); + let mut nonce_counter: BTreeMap<(Identifier, Identifier), u64> = BTreeMap::new(); + + // empty outer iter + let empty: Vec<( + DocumentTransitionActionType, + Vec<(Document, DocumentTypeRef, Bytes32, Option)>, + )> = vec![]; + let result = factory.create_state_transition(empty, &mut nonce_counter); + assert!( + matches!( + result, + Err(ProtocolError::Document(e)) if matches!(*e, DocumentError::NoDocumentsSuppliedError) + ), + "expected NoDocumentsSuppliedError" + ); + } + + #[test] + fn create_state_transition_mismatched_owners_returns_error() { + let (factory, data_contract) = setup_factory(); + let owner_a = Identifier::from([1u8; 32]); + let owner_b = Identifier::from([2u8; 32]); + let doc_a = build_document(&factory, owner_a, "noTimeDocument"); + let doc_b = build_document(&factory, owner_b, "noTimeDocument"); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Create, + vec![ + (doc_a, doc_type, Bytes32::new([1u8; 32]), None), + (doc_b, doc_type, Bytes32::new([2u8; 32]), None), + ], + )]; + + let result = factory.create_state_transition(entries, &mut nonce_counter); + assert!( + matches!( + result, + Err(ProtocolError::Document(e)) + if matches!(*e, DocumentError::MismatchOwnerIdsError { .. }) + ), + "expected MismatchOwnerIdsError" + ); + } + + #[test] + fn create_state_transition_replace_on_mutable_document_increments_revision() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([3u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let doc = build_document(&factory, owner_id, "noTimeDocument"); + // noTimeDocument is mutable by default → revision is Some(1) + assert_eq!(doc.revision(), Some(INITIAL_REVISION)); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Replace, + vec![(doc, doc_type, Bytes32::default(), None)], + )]; + + let batch = factory + .create_state_transition(entries, &mut nonce_counter) + .expect("replace transition should be built"); + assert_eq!(batch.transitions_len(), 1); + assert_eq!(batch.owner_id(), owner_id); + let key = (owner_id, data_contract.id()); + assert_eq!(*nonce_counter.get(&key).unwrap(), 1); + } + + #[test] + fn create_state_transition_replace_without_revision_returns_error() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([4u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let mut doc = build_document(&factory, owner_id, "noTimeDocument"); + // Remove revision to trigger RevisionAbsentError. + doc.set_revision(None); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Replace, + vec![(doc, doc_type, Bytes32::default(), None)], + )]; + let result = factory.create_state_transition(entries, &mut nonce_counter); + assert!( + matches!( + result, + Err(ProtocolError::Document(e)) + if matches!(*e, DocumentError::RevisionAbsentError { .. }) + ), + "expected RevisionAbsentError" + ); + } + + #[test] + fn create_state_transition_create_with_wrong_initial_revision_returns_error() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([5u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let mut doc = build_document(&factory, owner_id, "noTimeDocument"); + // Invalid initial revision: must be INITIAL_REVISION (1). + doc.set_revision(Some(42)); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Create, + vec![(doc, doc_type, Bytes32::default(), None)], + )]; + let result = factory.create_state_transition(entries, &mut nonce_counter); + assert!( + matches!( + result, + Err(ProtocolError::Document(e)) + if matches!(*e, DocumentError::InvalidInitialRevisionError { .. }) + ), + "expected InvalidInitialRevisionError" + ); + } + + #[test] + fn create_state_transition_create_without_revision_on_mutable_returns_error() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([6u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let mut doc = build_document(&factory, owner_id, "noTimeDocument"); + // For mutable documents, revision is required. + doc.set_revision(None); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Create, + vec![(doc, doc_type, Bytes32::default(), None)], + )]; + let result = factory.create_state_transition(entries, &mut nonce_counter); + assert!( + matches!( + result, + Err(ProtocolError::Document(e)) + if matches!(*e, DocumentError::RevisionAbsentError { .. }) + ), + "expected RevisionAbsentError" + ); + } + + #[test] + fn create_state_transition_delete_with_mutable_doc() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([7u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let doc = build_document(&factory, owner_id, "noTimeDocument"); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Delete, + vec![(doc, doc_type, Bytes32::default(), None)], + )]; + let batch = factory + .create_state_transition(entries, &mut nonce_counter) + .expect("delete transition should be built"); + assert_eq!(batch.transitions_len(), 1); + let key = (owner_id, data_contract.id()); + assert_eq!(*nonce_counter.get(&key).unwrap(), 1); + } + + #[test] + fn create_state_transition_delete_without_revision_returns_error() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([8u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let mut doc = build_document(&factory, owner_id, "noTimeDocument"); + doc.set_revision(None); + + let mut nonce_counter = BTreeMap::new(); + let entries = vec![( + DocumentTransitionActionType::Delete, + vec![(doc, doc_type, Bytes32::default(), None)], + )]; + let result = factory.create_state_transition(entries, &mut nonce_counter); + assert!( + matches!( + result, + Err(ProtocolError::Document(e)) + if matches!(*e, DocumentError::RevisionAbsentError { .. }) + ), + "expected RevisionAbsentError for delete without revision" + ); + } + + #[test] + fn create_state_transition_nonces_increment_per_document() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([9u8; 32]); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + let d1 = build_document(&factory, owner_id, "noTimeDocument"); + // produce a second doc with distinct id via different entropy/type path — + // the entropy comes from the factory, but owner is same; we reuse same doc type + // but set distinct revision-preserving id manually not needed, because nonce counter + // is keyed by (owner, contract). + let mut d2 = build_document(&factory, owner_id, "noTimeDocument"); + // ensure distinct id so the two can legitimately coexist + d2.set_id(Identifier::from([0xEEu8; 32])); + + let mut nonce_counter = BTreeMap::new(); + // pre-seed a nonce so we can assert the post-value is base + 2 + nonce_counter.insert((owner_id, data_contract.id()), 10); + + let entries = vec![( + DocumentTransitionActionType::Create, + vec![ + (d1, doc_type, Bytes32::new([1u8; 32]), None), + (d2, doc_type, Bytes32::new([2u8; 32]), None), + ], + )]; + let _ = factory + .create_state_transition(entries, &mut nonce_counter) + .expect("transition should build"); + + assert_eq!( + *nonce_counter.get(&(owner_id, data_contract.id())).unwrap(), + 12 + ); + } + } + + #[cfg(feature = "extended-document")] + mod extended_document_tests { + use super::*; + use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0; + + #[test] + fn create_extended_from_document_buffer_roundtrips() { + let (factory, data_contract) = setup_factory(); + let owner_id = Identifier::from([77u8; 32]); + + let doc = factory + .create_document_without_time_based_properties( + owner_id, + "noTimeDocument".to_string(), + platform_value!({ "name": "bob" }), + ) + .expect("doc should be created"); + let doc_type = data_contract + .document_type_for_name("noTimeDocument") + .unwrap(); + + let platform_version = PlatformVersion::latest(); + let bytes = doc + .serialize(doc_type, &data_contract, platform_version) + .expect("serialize"); + + let extended = factory + .create_extended_from_document_buffer( + bytes.as_slice(), + "noTimeDocument", + platform_version, + ) + .expect("extended doc should deserialize"); + + assert_eq!(extended.data_contract_id(), data_contract.id()); + assert_eq!(extended.document_type_name(), "noTimeDocument"); + } + + #[test] + fn create_extended_from_document_buffer_invalid_type_fails() { + let (factory, _) = setup_factory(); + let platform_version = PlatformVersion::latest(); + let result = factory.create_extended_from_document_buffer( + &[0u8; 8], + "doesNotExist", + platform_version, + ); + assert!(result.is_err()); + } + + #[test] + fn create_extended_from_document_buffer_bad_bytes_fails() { + let (factory, _) = setup_factory(); + let platform_version = PlatformVersion::latest(); + let result = factory.create_extended_from_document_buffer( + &[0xFFu8; 4], + "noTimeDocument", + platform_version, + ); + assert!(result.is_err()); + } + } } diff --git a/packages/rs-dpp/src/identity/identity_factory.rs b/packages/rs-dpp/src/identity/identity_factory.rs index f5504e24334..4a54c081d9d 100644 --- a/packages/rs-dpp/src/identity/identity_factory.rs +++ b/packages/rs-dpp/src/identity/identity_factory.rs @@ -415,4 +415,337 @@ mod tests { let identity = factory.create(id, public_keys).unwrap(); assert_eq!(identity.public_keys().len(), 1); } + + // ----- extended coverage ----- + + #[cfg(all(feature = "identity-serialization", feature = "client"))] + mod serialization_tests { + use super::*; + use crate::serialization::PlatformSerializable; + + fn latest_version() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + #[test] + fn create_from_buffer_roundtrip_empty_keys() { + let factory = IdentityFactory::new(latest_version().protocol_version); + let id = Identifier::from([9u8; 32]); + let identity = factory.create(id, BTreeMap::new()).unwrap(); + + let bytes = PlatformSerializable::serialize_to_bytes(&identity).unwrap(); + + let roundtripped = factory + .create_from_buffer( + bytes, + #[cfg(feature = "validation")] + true, + ) + .unwrap(); + + assert_eq!(roundtripped.id(), identity.id()); + assert_eq!(roundtripped.balance(), 0); + assert_eq!(roundtripped.revision(), 0); + assert_eq!(roundtripped.public_keys().len(), 0); + } + + #[test] + fn create_from_buffer_fails_on_garbage() { + let factory = IdentityFactory::new(latest_version().protocol_version); + let garbage = vec![0xFFu8; 16]; + + let result = factory.create_from_buffer( + garbage, + #[cfg(feature = "validation")] + true, + ); + assert!(result.is_err()); + } + + #[test] + fn create_from_buffer_fails_on_empty() { + let factory = IdentityFactory::new(latest_version().protocol_version); + let result = factory.create_from_buffer( + Vec::new(), + #[cfg(feature = "validation")] + true, + ); + assert!(result.is_err()); + } + } + + #[cfg(all(feature = "state-transitions", feature = "client"))] + mod state_transition_tests { + use super::*; + use crate::identity::core_script::CoreScript; + use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; + use crate::identity::state_transition::asset_lock_proof::AssetLockProof; + use crate::identity::{KeyType, Purpose, SecurityLevel}; + use crate::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; + use crate::state_transition::identity_credit_withdrawal_transition::accessors::IdentityCreditWithdrawalTransitionAccessorsV0; + use crate::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; + use crate::state_transition::identity_update_transition::accessors::IdentityUpdateTransitionAccessorsV0; + use crate::tests::fixtures::instant_asset_lock_proof_fixture; + use crate::withdrawal::Pooling; + + fn sample_public_key(id: u32) -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::MASTER, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + read_only: false, + data: vec![0u8; 33].into(), + disabled_at: None, + }) + } + + #[test] + fn create_identity_create_transition_from_identity() { + let factory = IdentityFactory::new(1); + let asset_lock_proof = instant_asset_lock_proof_fixture(None, None); + let expected_identifier = asset_lock_proof.create_identifier().unwrap(); + + let identity = factory + .create(expected_identifier, BTreeMap::new()) + .unwrap(); + + let transition = factory + .create_identity_create_transition(&identity, asset_lock_proof) + .expect("expected transition"); + + match transition { + IdentityCreateTransition::V0(v0) => { + assert_eq!(v0.identity_id, expected_identifier); + assert!(v0.public_keys.is_empty()); + } + } + } + + #[test] + fn create_identity_with_create_transition_computes_id_from_asset_lock_proof() { + let factory = IdentityFactory::new(1); + let asset_lock_proof = instant_asset_lock_proof_fixture(None, None); + let expected_id = asset_lock_proof.create_identifier().unwrap(); + + let mut public_keys = BTreeMap::new(); + public_keys.insert(0u32, sample_public_key(0)); + + let (identity, transition) = factory + .create_identity_with_create_transition(public_keys, asset_lock_proof) + .unwrap(); + + assert_eq!(identity.id(), expected_id); + assert_eq!(identity.balance(), 0); + assert_eq!(identity.revision(), 0); + assert_eq!(identity.public_keys().len(), 1); + + match transition { + IdentityCreateTransition::V0(v0) => { + assert_eq!(v0.identity_id, expected_id); + assert_eq!(v0.public_keys.len(), 1); + } + } + } + + #[test] + fn create_identity_topup_transition_sets_identity_and_proof() { + let factory = IdentityFactory::new(1); + let identity_id = Identifier::from([3u8; 32]); + let asset_lock_proof = instant_asset_lock_proof_fixture(None, None); + + let transition = factory + .create_identity_topup_transition(identity_id, asset_lock_proof.clone()) + .unwrap(); + + assert_eq!(*transition.identity_id(), identity_id); + // asset lock proof preserved + match &transition { + IdentityTopUpTransition::V0(v0) => match &v0.asset_lock_proof { + AssetLockProof::Instant(_) => {} + _ => panic!("expected instant asset lock proof"), + }, + } + } + + #[test] + fn create_identity_credit_transfer_transition_fields() { + let factory = IdentityFactory::new(1); + let id = Identifier::from([11u8; 32]); + let identity = factory.create(id, BTreeMap::new()).unwrap(); + let recipient = Identifier::from([22u8; 32]); + let amount = 123_456u64; + let nonce = 7u64; + + let transition = factory + .create_identity_credit_transfer_transition(&identity, recipient, amount, nonce) + .unwrap(); + + assert_eq!(transition.identity_id(), id); + assert_eq!(transition.recipient_id(), recipient); + assert_eq!(transition.amount(), amount); + assert_eq!(transition.nonce(), nonce); + } + + #[test] + fn create_identity_credit_withdrawal_transition_v0_requires_output_script() { + // Protocol version 1 uses the v0 constructor, which REQUIRES output_script. + let factory = IdentityFactory::new(1); + let identity_id = Identifier::from([5u8; 32]); + + let result = factory.create_identity_credit_withdrawal_transition( + identity_id, + 1000, + 1, + Pooling::Never, + None, // missing output script -> error + 1, + ); + match result { + Err(ProtocolError::Generic(msg)) => { + assert!(msg.contains("Output script is required")); + } + other => panic!("expected Generic error, got {:?}", other), + } + } + + #[test] + fn create_identity_credit_withdrawal_transition_v0_with_output_script_succeeds() { + let factory = IdentityFactory::new(1); + let identity_id = Identifier::from([5u8; 32]); + let script = CoreScript::new_p2pkh([0xAB; 20]); + + let transition = factory + .create_identity_credit_withdrawal_transition( + identity_id, + 10_000, + 2, + Pooling::IfAvailable, + Some(script.clone()), + 42, + ) + .unwrap(); + + assert_eq!(transition.identity_id(), identity_id); + assert_eq!(transition.amount(), 10_000); + assert_eq!(transition.core_fee_per_byte(), 2); + assert_eq!(transition.pooling(), Pooling::IfAvailable); + assert_eq!(transition.output_script(), Some(script)); + assert_eq!(transition.nonce(), 42); + // Protocol v1 maps to V0 variant. + match transition { + IdentityCreditWithdrawalTransition::V0(_) => {} + other => panic!("expected V0 variant, got {:?}", other), + } + } + + #[test] + fn create_identity_credit_withdrawal_transition_v1_accepts_none_output_script() { + // The latest platform version uses the v1 constructor where output_script is optional. + let latest = PlatformVersion::latest(); + let factory = IdentityFactory::new(latest.protocol_version); + let identity_id = Identifier::from([6u8; 32]); + + let transition = factory + .create_identity_credit_withdrawal_transition( + identity_id, + 500, + 3, + Pooling::Standard, + None, + 99, + ) + .unwrap(); + + assert_eq!(transition.identity_id(), identity_id); + assert_eq!(transition.amount(), 500); + assert_eq!(transition.pooling(), Pooling::Standard); + assert_eq!(transition.nonce(), 99); + assert_eq!(transition.output_script(), None); + match transition { + IdentityCreditWithdrawalTransition::V1(_) => {} + other => panic!("expected V1 variant, got {:?}", other), + } + } + + #[test] + fn create_identity_credit_withdrawal_transition_invalid_version() { + let factory = IdentityFactory::new(u32::MAX); + let identity_id = Identifier::from([5u8; 32]); + + let result = factory.create_identity_credit_withdrawal_transition( + identity_id, + 1000, + 1, + Pooling::Never, + Some(CoreScript::new_p2pkh([0u8; 20])), + 1, + ); + assert!(result.is_err()); + } + + #[test] + fn create_identity_update_transition_increments_revision_and_applies_adds() { + let factory = IdentityFactory::new(1); + let id = Identifier::from([7u8; 32]); + let mut identity = factory.create(id, BTreeMap::new()).unwrap(); + // give starting revision some non-zero value + match &mut identity { + Identity::V0(v0) => { + v0.revision = 5; + } + } + + let new_key: IdentityPublicKeyInCreation = + IdentityPublicKey::from(sample_public_key(1)).into(); + let update = factory + .create_identity_update_transition(identity, 33, Some(vec![new_key.clone()]), None) + .unwrap(); + + assert_eq!(update.identity_id(), id); + assert_eq!(update.revision(), 6); // was 5, +1 + assert_eq!(update.nonce(), 33); + assert_eq!(update.public_keys_to_add().len(), 1); + assert!(update.public_key_ids_to_disable().is_empty()); + } + + #[test] + fn create_identity_update_transition_with_disabled_keys() { + let factory = IdentityFactory::new(1); + let id = Identifier::from([8u8; 32]); + let identity = factory.create(id, BTreeMap::new()).unwrap(); + + let update = factory + .create_identity_update_transition(identity, 1, None, Some(vec![2u32, 3u32, 5u32])) + .unwrap(); + + assert_eq!(update.identity_id(), id); + // initial revision is 0, +1 + assert_eq!(update.revision(), 1); + assert_eq!(update.nonce(), 1); + assert!(update.public_keys_to_add().is_empty()); + assert_eq!(update.public_key_ids_to_disable(), &[2u32, 3, 5][..]); + } + + #[test] + fn create_instant_lock_proof_preserves_output_index() { + let asset_lock_proof = instant_asset_lock_proof_fixture(None, None); + let tx = match &asset_lock_proof { + AssetLockProof::Instant(p) => p.transaction.clone(), + _ => panic!("expected Instant"), + }; + let il = match &asset_lock_proof { + AssetLockProof::Instant(p) => p.instant_lock.clone(), + _ => panic!("expected Instant"), + }; + + let built = IdentityFactory::create_instant_lock_proof(il.clone(), tx.clone(), 7); + assert_eq!(built.output_index, 7); + assert_eq!(built.transaction.txid(), tx.txid()); + // round trip back to AssetLockProof + let as_proof = AssetLockProof::Instant(built.clone()); + assert!(matches!(as_proof, AssetLockProof::Instant(_))); + } + } } diff --git a/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs index 45157bc5105..4d261d1cb56 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/contract_bounds/mod.rs @@ -163,6 +163,122 @@ impl ContractBounds { // } } +#[cfg(test)] +mod core_tests { + use super::*; + + // -- new_from_type: valid types -- + #[test] + fn test_new_from_type_single_contract() { + let id_bytes = vec![0xAAu8; 32]; + let bounds = + ContractBounds::new_from_type(0, id_bytes.clone(), "ignored".to_string()).unwrap(); + assert!(matches!(bounds, ContractBounds::SingleContract { .. })); + assert_eq!(bounds.contract_bounds_type(), 0); + assert_eq!(bounds.contract_bounds_type_string(), "singleContract"); + assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice()); + // document_type is None for SingleContract regardless of what we passed in. + assert!(bounds.document_type().is_none()); + } + + #[test] + fn test_new_from_type_single_contract_document_type() { + let id_bytes = vec![0xBBu8; 32]; + let bounds = ContractBounds::new_from_type(1, id_bytes.clone(), "myDoc".to_string()) + .expect("expected to construct SingleContractDocumentType"); + assert!(matches!( + bounds, + ContractBounds::SingleContractDocumentType { .. } + )); + assert_eq!(bounds.contract_bounds_type(), 1); + assert_eq!(bounds.contract_bounds_type_string(), "documentType"); + assert_eq!(bounds.identifier().as_bytes(), id_bytes.as_slice()); + assert_eq!(bounds.document_type().map(String::as_str), Some("myDoc")); + } + + // -- new_from_type: invalid type -- + #[test] + fn test_new_from_type_unrecognized_type_returns_error() { + let id_bytes = vec![0xCCu8; 32]; + let err = ContractBounds::new_from_type(99, id_bytes, "".to_string()).unwrap_err(); + match err { + ProtocolError::InvalidKeyContractBoundsError(msg) => { + assert!(msg.contains("99"), "expected error message to mention 99"); + } + other => panic!("expected InvalidKeyContractBoundsError, got {:?}", other), + } + } + + // -- new_from_type: identifier wrong length -- + #[test] + fn test_new_from_type_invalid_identifier_length_returns_error() { + // Identifier::from_bytes requires exactly 32 bytes. + let short = vec![0x01u8; 10]; + assert!(ContractBounds::new_from_type(0, short, "".to_string()).is_err()); + } + + // -- contract_bounds_type_from_str -- + #[test] + fn test_contract_bounds_type_from_str_single_contract() { + assert_eq!( + ContractBounds::contract_bounds_type_from_str("singleContract").unwrap(), + 0 + ); + } + + #[test] + fn test_contract_bounds_type_from_str_document_type() { + assert_eq!( + ContractBounds::contract_bounds_type_from_str("documentType").unwrap(), + 1 + ); + } + + #[test] + fn test_contract_bounds_type_from_str_unknown_returns_error() { + let err = ContractBounds::contract_bounds_type_from_str("garbage").unwrap_err(); + match err { + ProtocolError::DecodingError(_) => {} + other => panic!("expected ProtocolError::DecodingError, got {:?}", other), + } + } + + // -- equality / clone / hash (derives) -- + #[test] + fn test_contract_bounds_equality_and_clone() { + let id = Identifier::from([0x11u8; 32]); + let a = ContractBounds::SingleContract { id }; + let b = a.clone(); + assert_eq!(a, b); + + let different = ContractBounds::SingleContractDocumentType { + id, + document_type_name: "foo".to_string(), + }; + assert_ne!(a, different); + } + + #[test] + fn test_contract_bounds_type_string_roundtrip_with_from_str() { + // The string form produced by the variant should round-trip through from_str + // back to its numeric discriminant. + let id_bytes = vec![0xD0u8; 32]; + let sc = ContractBounds::new_from_type(0, id_bytes.clone(), "".to_string()).unwrap(); + let sctd = ContractBounds::new_from_type(1, id_bytes, "docType".to_string()).unwrap(); + + assert_eq!( + ContractBounds::contract_bounds_type_from_str(sc.contract_bounds_type_string()) + .unwrap(), + sc.contract_bounds_type() + ); + assert_eq!( + ContractBounds::contract_bounds_type_from_str(sctd.contract_bounds_type_string()) + .unwrap(), + sctd.contract_bounds_type() + ); + } +} + #[cfg(all(test, feature = "json-conversion"))] mod tests { use super::*; diff --git a/packages/rs-dpp/src/identity/identity_public_key/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/mod.rs index f06aaafa11f..541abde9ad3 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/mod.rs @@ -113,9 +113,14 @@ impl IdentityPublicKey { #[cfg(test)] mod tests { + use crate::identity::identity_public_key::accessors::v0::{ + IdentityPublicKeyGettersV0, IdentityPublicKeySettersV0, + }; + use crate::identity::identity_public_key::contract_bounds::ContractBounds; use crate::identity::identity_public_key::v0::IdentityPublicKeyV0; - use crate::identity::IdentityPublicKey; + use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; use crate::serialization::{PlatformDeserializable, PlatformSerializable}; + use platform_value::{BinaryData, Identifier}; use platform_version::version::LATEST_PLATFORM_VERSION; use rand::SeedableRng; @@ -137,4 +142,413 @@ mod tests { .expect("expected to deserialize key"); assert_eq!(key, unserialized) } + + // -- is_master -- + + fn make_key_v0(security_level: SecurityLevel) -> IdentityPublicKey { + IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 42, + purpose: Purpose::AUTHENTICATION, + security_level, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + data: BinaryData::new(vec![0u8; 33]), + read_only: false, + disabled_at: None, + }) + } + + #[test] + fn test_is_master_true_when_security_level_master() { + let key = make_key_v0(SecurityLevel::MASTER); + assert!(key.is_master()); + } + + #[test] + fn test_is_master_false_for_other_levels() { + assert!(!make_key_v0(SecurityLevel::CRITICAL).is_master()); + assert!(!make_key_v0(SecurityLevel::HIGH).is_master()); + assert!(!make_key_v0(SecurityLevel::MEDIUM).is_master()); + } + + // -- max_possible_size_key -- + + #[test] + fn test_max_possible_size_key_v0_returns_bls_sized_data() { + // BLS12_381 has the largest default size (48 bytes). + let key = IdentityPublicKey::max_possible_size_key(123, LATEST_PLATFORM_VERSION) + .expect("max_possible_size_key should work for v0"); + assert_eq!(key.id(), 123); + assert_eq!(key.purpose(), Purpose::AUTHENTICATION); + assert_eq!(key.security_level(), SecurityLevel::MASTER); + assert!(!key.read_only()); + assert!(key.contract_bounds().is_none()); + assert!(key.disabled_at().is_none()); + // data should be default_size() bytes filled with 0xff. + assert_eq!(key.data().len(), key.key_type().default_size()); + assert!(key.data().as_slice().iter().all(|b| *b == 0xff)); + } + + // -- default_versioned -- + + #[test] + fn test_default_versioned_returns_v0_default() { + let key = IdentityPublicKey::default_versioned(LATEST_PLATFORM_VERSION) + .expect("default_versioned should work for v0"); + // The V0 Default should have id=0 and default purpose/security_level/key_type. + assert_eq!(key.id(), 0); + assert_eq!(key.purpose(), Purpose::default()); + assert_eq!(key.security_level(), SecurityLevel::default()); + assert_eq!(key.key_type(), KeyType::default()); + } + + // -- accessors on the IdentityPublicKey enum wrapper -- + + #[test] + fn test_wrapper_getters_delegate_to_v0() { + let inner = IdentityPublicKeyV0 { + id: 7, + purpose: Purpose::TRANSFER, + security_level: SecurityLevel::CRITICAL, + contract_bounds: Some(ContractBounds::SingleContract { + id: Identifier::from([0x01u8; 32]), + }), + key_type: KeyType::BLS12_381, + data: BinaryData::new(vec![2u8; 48]), + read_only: true, + disabled_at: Some(1_700_000_000_000), + }; + let key = IdentityPublicKey::V0(inner.clone()); + assert_eq!(key.id(), 7); + assert_eq!(key.purpose(), Purpose::TRANSFER); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + assert_eq!(key.key_type(), KeyType::BLS12_381); + assert!(key.read_only()); + assert_eq!(key.data().as_slice(), &vec![2u8; 48][..]); + assert_eq!(key.disabled_at(), Some(1_700_000_000_000)); + assert!(key.is_disabled()); + assert!(key.contract_bounds().is_some()); + + // data_owned returns the underlying data. + let cloned = key.clone(); + assert_eq!(cloned.data_owned().as_slice(), &vec![2u8; 48][..]); + } + + #[test] + fn test_wrapper_setters_delegate_to_v0() { + let mut key = make_key_v0(SecurityLevel::HIGH); + key.set_id(99); + key.set_purpose(Purpose::ENCRYPTION); + key.set_security_level(SecurityLevel::CRITICAL); + key.set_key_type(KeyType::BLS12_381); + key.set_read_only(true); + key.set_data(BinaryData::new(vec![0xABu8; 48])); + key.set_disabled_at(1234); + + assert_eq!(key.id(), 99); + assert_eq!(key.purpose(), Purpose::ENCRYPTION); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + assert_eq!(key.key_type(), KeyType::BLS12_381); + assert!(key.read_only()); + assert_eq!(key.data().as_slice(), &vec![0xABu8; 48][..]); + assert_eq!(key.disabled_at(), Some(1234)); + assert!(key.is_disabled()); + + key.remove_disabled_at(); + assert_eq!(key.disabled_at(), None); + assert!(!key.is_disabled()); + } + + // -- From for IdentityPublicKey -- + #[test] + fn test_from_v0_into_wrapper() { + let v0 = IdentityPublicKeyV0 { + id: 5, + purpose: Purpose::VOTING, + security_level: SecurityLevel::MEDIUM, + contract_bounds: None, + key_type: KeyType::ECDSA_HASH160, + data: BinaryData::new(vec![0u8; 20]), + read_only: false, + disabled_at: None, + }; + let wrapped: IdentityPublicKey = v0.clone().into(); + assert_eq!(wrapped.id(), 5); + assert_eq!(wrapped.purpose(), Purpose::VOTING); + assert_eq!(wrapped.security_level(), SecurityLevel::MEDIUM); + // Equality on the enum variant. + assert_eq!(wrapped, IdentityPublicKey::V0(v0)); + } + + // -- IdentityPublicKeyV0::default basic invariants -- + #[test] + fn test_v0_default_fields() { + let v0 = IdentityPublicKeyV0::default(); + assert_eq!(v0.id, 0); + assert_eq!(v0.purpose, Purpose::default()); + assert_eq!(v0.security_level, SecurityLevel::default()); + assert_eq!(v0.key_type, KeyType::default()); + assert!(!v0.read_only); + assert_eq!(v0.data.as_slice().len(), 0); + assert!(v0.disabled_at.is_none()); + assert!(v0.contract_bounds.is_none()); + } + + // -- Ordering is derived: Clone + Eq + PartialOrd -- + #[test] + fn test_keys_are_orderable_and_cloneable() { + let a = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 1, + ..Default::default() + }); + let b = IdentityPublicKey::V0(IdentityPublicKeyV0 { + id: 2, + ..Default::default() + }); + assert!(a < b); + let a_clone = a.clone(); + assert_eq!(a, a_clone); + } +} + +#[cfg(all(test, feature = "random-public-keys"))] +mod random_tests { + use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; + use platform_version::version::LATEST_PLATFORM_VERSION; + + // -- random_key and random_keys with a seed are deterministic. -- + + #[test] + fn test_random_key_with_seed_is_deterministic() { + let k1 = IdentityPublicKey::random_key(1, Some(42), LATEST_PLATFORM_VERSION); + let k2 = IdentityPublicKey::random_key(1, Some(42), LATEST_PLATFORM_VERSION); + assert_eq!(k1, k2); + } + + #[test] + fn test_random_keys_returns_requested_count() { + let keys = IdentityPublicKey::random_keys(0, 5, Some(7), LATEST_PLATFORM_VERSION); + assert_eq!(keys.len(), 5); + // Each should have a distinct id in [0, 5). + let mut ids: Vec<_> = keys.iter().map(|k| k.id()).collect(); + ids.sort(); + assert_eq!(ids, vec![0, 1, 2, 3, 4]); + } + + #[test] + fn test_random_authentication_key_is_always_authentication_purpose() { + for id in 0..5u32 { + let k = IdentityPublicKey::random_authentication_key( + id, + Some(id as u64), + LATEST_PLATFORM_VERSION, + ); + assert_eq!(k.purpose(), Purpose::AUTHENTICATION); + } + } + + #[test] + fn test_random_authentication_keys_returns_requested_count_and_all_auth() { + let keys = + IdentityPublicKey::random_authentication_keys(0, 3, Some(11), LATEST_PLATFORM_VERSION); + assert_eq!(keys.len(), 3); + for k in &keys { + assert_eq!(k.purpose(), Purpose::AUTHENTICATION); + } + } + + #[test] + fn test_random_ecdsa_master_authentication_key_has_expected_attributes() { + let (key, _priv) = IdentityPublicKey::random_ecdsa_master_authentication_key( + 0, + Some(1), + LATEST_PLATFORM_VERSION, + ) + .expect("expected master authentication key"); + assert_eq!(key.key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(key.purpose(), Purpose::AUTHENTICATION); + assert_eq!(key.security_level(), SecurityLevel::MASTER); + assert!(!key.read_only()); + assert_eq!(key.data().len(), 33); + } + + #[test] + fn test_random_ecdsa_critical_level_authentication_key_attributes() { + let (key, _priv) = IdentityPublicKey::random_ecdsa_critical_level_authentication_key( + 1, + Some(2), + LATEST_PLATFORM_VERSION, + ) + .expect("expected critical auth key"); + assert_eq!(key.key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(key.purpose(), Purpose::AUTHENTICATION); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + } + + #[test] + fn test_random_ecdsa_high_level_authentication_key_attributes() { + let (key, _priv) = IdentityPublicKey::random_ecdsa_high_level_authentication_key( + 2, + Some(3), + LATEST_PLATFORM_VERSION, + ) + .expect("expected high-level auth key"); + assert_eq!(key.key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(key.purpose(), Purpose::AUTHENTICATION); + assert_eq!(key.security_level(), SecurityLevel::HIGH); + } + + #[test] + fn test_random_masternode_owner_key_has_owner_purpose_and_is_read_only() { + let (key, _priv) = + IdentityPublicKey::random_masternode_owner_key(3, Some(4), LATEST_PLATFORM_VERSION) + .expect("expected masternode owner key"); + assert_eq!(key.key_type(), KeyType::ECDSA_HASH160); + assert_eq!(key.purpose(), Purpose::OWNER); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + assert!(key.read_only()); + } + + #[test] + fn test_random_masternode_transfer_key_has_transfer_purpose_and_is_read_only() { + let (key, _priv) = + IdentityPublicKey::random_masternode_transfer_key(4, Some(5), LATEST_PLATFORM_VERSION) + .expect("expected masternode transfer key"); + assert_eq!(key.key_type(), KeyType::ECDSA_HASH160); + assert_eq!(key.purpose(), Purpose::TRANSFER); + assert_eq!(key.security_level(), SecurityLevel::CRITICAL); + assert!(key.read_only()); + } + + #[test] + fn test_main_keys_with_random_authentication_keys_errors_when_count_below_two() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(1); + let err = + IdentityPublicKey::main_keys_with_random_authentication_keys_with_private_keys_with_rng( + 1, + &mut rng, + LATEST_PLATFORM_VERSION, + ) + .unwrap_err(); + match err { + crate::ProtocolError::PublicKeyGenerationError(msg) => { + assert!(msg.contains("at least 2")); + } + other => panic!("expected PublicKeyGenerationError, got {:?}", other), + } + } + + #[test] + fn test_main_keys_with_random_authentication_keys_count_two() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(2); + let keys = IdentityPublicKey::main_keys_with_random_authentication_keys_with_private_keys_with_rng( + 2, + &mut rng, + LATEST_PLATFORM_VERSION, + ) + .expect("expected keys"); + assert_eq!(keys.len(), 2); + // Position 0 must be Master ECDSA, position 1 must be High ECDSA. + assert_eq!(keys[0].0.security_level(), SecurityLevel::MASTER); + assert_eq!(keys[0].0.key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(keys[1].0.security_level(), SecurityLevel::HIGH); + assert_eq!(keys[1].0.key_type(), KeyType::ECDSA_SECP256K1); + } + + #[test] + fn test_main_keys_with_random_authentication_keys_count_three() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(3); + let keys = IdentityPublicKey::main_keys_with_random_authentication_keys_with_private_keys_with_rng( + 3, + &mut rng, + LATEST_PLATFORM_VERSION, + ) + .expect("expected keys"); + assert_eq!(keys.len(), 3); + assert_eq!(keys[0].0.security_level(), SecurityLevel::MASTER); + assert_eq!(keys[1].0.security_level(), SecurityLevel::CRITICAL); + assert_eq!(keys[2].0.security_level(), SecurityLevel::HIGH); + } + + #[test] + fn test_random_unique_keys_with_rng_matches_count() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(9); + let keys = + IdentityPublicKey::random_unique_keys_with_rng(4, &mut rng, LATEST_PLATFORM_VERSION) + .expect("expected keys"); + assert_eq!(keys.len(), 4); + // IDs should be 0..4. + let ids: Vec<_> = keys.iter().map(|k| k.id()).collect(); + assert_eq!(ids, vec![0, 1, 2, 3]); + } + + #[test] + fn test_random_keys_with_rng_returns_requested_count() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(10); + let keys = IdentityPublicKey::random_keys_with_rng(3, &mut rng, LATEST_PLATFORM_VERSION); + assert_eq!(keys.len(), 3); + } + + #[test] + fn test_random_authentication_keys_with_private_keys_with_rng_returns_pairs() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(11); + let pairs = IdentityPublicKey::random_authentication_keys_with_private_keys_with_rng( + 5, + 3, + &mut rng, + LATEST_PLATFORM_VERSION, + ) + .expect("expected pairs"); + assert_eq!(pairs.len(), 3); + let ids: Vec<_> = pairs.iter().map(|(k, _)| k.id()).collect(); + assert_eq!(ids, vec![5, 6, 7]); + for (k, _) in &pairs { + assert_eq!(k.purpose(), Purpose::AUTHENTICATION); + } + } + + #[test] + fn test_random_voting_key_with_rng_attributes() { + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(13); + let (k, _priv) = + IdentityPublicKey::random_voting_key_with_rng(0, &mut rng, LATEST_PLATFORM_VERSION) + .expect("expected voting key"); + assert_eq!(k.purpose(), Purpose::VOTING); + assert_eq!(k.key_type(), KeyType::ECDSA_HASH160); + assert_eq!(k.security_level(), SecurityLevel::MEDIUM); + } + + #[test] + fn test_random_key_with_known_attributes_honors_inputs() { + use crate::identity::contract_bounds::ContractBounds; + use platform_value::Identifier; + use rand::{rngs::StdRng, SeedableRng}; + let mut rng = StdRng::seed_from_u64(17); + let bounds = ContractBounds::SingleContract { + id: Identifier::from([0x77u8; 32]), + }; + let (k, _priv) = IdentityPublicKey::random_key_with_known_attributes( + 42, + &mut rng, + Purpose::TRANSFER, + SecurityLevel::CRITICAL, + KeyType::ECDSA_SECP256K1, + Some(bounds.clone()), + LATEST_PLATFORM_VERSION, + ) + .expect("expected key"); + assert_eq!(k.id(), 42); + assert_eq!(k.purpose(), Purpose::TRANSFER); + assert_eq!(k.security_level(), SecurityLevel::CRITICAL); + assert_eq!(k.key_type(), KeyType::ECDSA_SECP256K1); + assert_eq!(k.contract_bounds(), Some(&bounds)); + } } diff --git a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs index fbd5fcb1cb5..61cb212f096 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/purpose.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/purpose.rs @@ -129,3 +129,143 @@ impl Purpose { [ENCRYPTION, DECRYPTION] } } + +#[cfg(test)] +mod tests { + use super::*; + + // -- TryFrom valid -- + #[test] + fn test_purpose_try_from_u8_valid_all_variants() { + assert_eq!(Purpose::try_from(0u8).unwrap(), AUTHENTICATION); + assert_eq!(Purpose::try_from(1u8).unwrap(), ENCRYPTION); + assert_eq!(Purpose::try_from(2u8).unwrap(), DECRYPTION); + assert_eq!(Purpose::try_from(3u8).unwrap(), TRANSFER); + assert_eq!(Purpose::try_from(4u8).unwrap(), SYSTEM); + assert_eq!(Purpose::try_from(5u8).unwrap(), VOTING); + assert_eq!(Purpose::try_from(6u8).unwrap(), OWNER); + } + + // -- TryFrom invalid -- + #[test] + fn test_purpose_try_from_u8_invalid() { + assert!(Purpose::try_from(7u8).is_err()); + assert!(Purpose::try_from(255u8).is_err()); + } + + // -- TryFrom valid + invalid -- + #[test] + fn test_purpose_try_from_i32_valid_all_variants() { + assert_eq!(Purpose::try_from(0i32).unwrap(), AUTHENTICATION); + assert_eq!(Purpose::try_from(1i32).unwrap(), ENCRYPTION); + assert_eq!(Purpose::try_from(2i32).unwrap(), DECRYPTION); + assert_eq!(Purpose::try_from(3i32).unwrap(), TRANSFER); + assert_eq!(Purpose::try_from(4i32).unwrap(), SYSTEM); + assert_eq!(Purpose::try_from(5i32).unwrap(), VOTING); + assert_eq!(Purpose::try_from(6i32).unwrap(), OWNER); + } + + #[test] + fn test_purpose_try_from_i32_invalid() { + assert!(Purpose::try_from(-1i32).is_err()); + assert!(Purpose::try_from(7i32).is_err()); + assert!(Purpose::try_from(1_000_000i32).is_err()); + } + + // -- From for [u8; 1] (by-value) -- + #[test] + fn test_purpose_to_owned_byte_array() { + let arr: [u8; 1] = AUTHENTICATION.into(); + assert_eq!(arr, [0]); + let arr: [u8; 1] = OWNER.into(); + assert_eq!(arr, [6]); + let arr: [u8; 1] = SYSTEM.into(); + assert_eq!(arr, [4]); + } + + // -- From for &'static [u8; 1] -- + #[test] + fn test_purpose_to_static_byte_ref_all_variants() { + let r: &'static [u8; 1] = AUTHENTICATION.into(); + assert_eq!(r, &[0u8]); + let r: &'static [u8; 1] = ENCRYPTION.into(); + assert_eq!(r, &[1u8]); + let r: &'static [u8; 1] = DECRYPTION.into(); + assert_eq!(r, &[2u8]); + let r: &'static [u8; 1] = TRANSFER.into(); + assert_eq!(r, &[3u8]); + let r: &'static [u8; 1] = SYSTEM.into(); + assert_eq!(r, &[4u8]); + let r: &'static [u8; 1] = VOTING.into(); + assert_eq!(r, &[5u8]); + let r: &'static [u8; 1] = OWNER.into(); + assert_eq!(r, &[6u8]); + } + + // -- Display (via Debug) -- + #[test] + fn test_purpose_display_matches_debug_form() { + assert_eq!(format!("{}", AUTHENTICATION), "AUTHENTICATION"); + assert_eq!(format!("{}", ENCRYPTION), "ENCRYPTION"); + assert_eq!(format!("{}", DECRYPTION), "DECRYPTION"); + assert_eq!(format!("{}", TRANSFER), "TRANSFER"); + assert_eq!(format!("{}", SYSTEM), "SYSTEM"); + assert_eq!(format!("{}", VOTING), "VOTING"); + assert_eq!(format!("{}", OWNER), "OWNER"); + } + + // -- Default -- + #[test] + fn test_purpose_default_is_authentication() { + assert_eq!(Purpose::default(), AUTHENTICATION); + } + + // -- Range helpers -- + #[test] + fn test_purpose_full_range_contents() { + // NOTE: full_range() intentionally excludes SYSTEM. + let full = Purpose::full_range(); + assert_eq!(full.len(), 6); + assert!(full.contains(&AUTHENTICATION)); + assert!(full.contains(&ENCRYPTION)); + assert!(full.contains(&DECRYPTION)); + assert!(full.contains(&TRANSFER)); + assert!(full.contains(&VOTING)); + assert!(full.contains(&OWNER)); + assert!(!full.contains(&SYSTEM)); + } + + #[test] + fn test_purpose_searchable_purposes_contents() { + let searchable = Purpose::searchable_purposes(); + assert_eq!(searchable.len(), 3); + assert_eq!(searchable, [AUTHENTICATION, TRANSFER, VOTING]); + } + + #[test] + fn test_purpose_encryption_decryption_contents() { + let ed = Purpose::encryption_decryption(); + assert_eq!(ed.len(), 2); + assert_eq!(ed, [ENCRYPTION, DECRYPTION]); + } + + // -- round-trip: Purpose -> u8 -> Purpose -- + #[test] + fn test_purpose_round_trip_u8() { + for val in 0u8..=6 { + let p = Purpose::try_from(val).unwrap(); + assert_eq!(p as u8, val); + } + } + + // -- ordering -- + #[test] + fn test_purpose_ordering_matches_discriminant() { + assert!(AUTHENTICATION < ENCRYPTION); + assert!(ENCRYPTION < DECRYPTION); + assert!(DECRYPTION < TRANSFER); + assert!(TRANSFER < SYSTEM); + assert!(SYSTEM < VOTING); + assert!(VOTING < OWNER); + } +} diff --git a/packages/rs-dpp/src/identity/identity_public_key/security_level.rs b/packages/rs-dpp/src/identity/identity_public_key/security_level.rs index 4cd9f0d2ed4..a907fd28104 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/security_level.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/security_level.rs @@ -111,3 +111,165 @@ impl std::fmt::Display for SecurityLevel { write!(f, "{self:?}") } } + +#[cfg(test)] +mod tests { + use super::*; + + // -- TryFrom valid -- + #[test] + fn test_security_level_try_from_u8_all_valid() { + assert_eq!(SecurityLevel::try_from(0u8).unwrap(), SecurityLevel::MASTER); + assert_eq!( + SecurityLevel::try_from(1u8).unwrap(), + SecurityLevel::CRITICAL + ); + assert_eq!(SecurityLevel::try_from(2u8).unwrap(), SecurityLevel::HIGH); + assert_eq!(SecurityLevel::try_from(3u8).unwrap(), SecurityLevel::MEDIUM); + } + + // -- TryFrom invalid returns UnknownSecurityLevelError -- + #[test] + fn test_security_level_try_from_u8_invalid_is_consensus_error() { + let err = SecurityLevel::try_from(4u8).unwrap_err(); + // Confirm it is a ProtocolError::ConsensusError wrapping BasicError::UnknownSecurityLevelError. + match err { + ProtocolError::ConsensusError(ce) => match *ce { + ConsensusError::BasicError(BasicError::UnknownSecurityLevelError(_)) => {} + other => panic!("unexpected inner consensus error: {:?}", other), + }, + other => panic!("expected ProtocolError::ConsensusError, got {:?}", other), + } + } + + #[test] + fn test_security_level_try_from_u8_invalid_255() { + assert!(SecurityLevel::try_from(255u8).is_err()); + } + + // -- From for [u8; 1] (owned) -- + #[test] + fn test_security_level_to_owned_byte_array() { + let arr: [u8; 1] = SecurityLevel::MASTER.into(); + assert_eq!(arr, [0]); + let arr: [u8; 1] = SecurityLevel::CRITICAL.into(); + assert_eq!(arr, [1]); + let arr: [u8; 1] = SecurityLevel::HIGH.into(); + assert_eq!(arr, [2]); + let arr: [u8; 1] = SecurityLevel::MEDIUM.into(); + assert_eq!(arr, [3]); + } + + // -- From for &'static [u8; 1] -- + #[test] + fn test_security_level_to_static_byte_ref_all_variants() { + let r: &'static [u8; 1] = SecurityLevel::MASTER.into(); + assert_eq!(r, &[0u8]); + let r: &'static [u8; 1] = SecurityLevel::CRITICAL.into(); + assert_eq!(r, &[1u8]); + let r: &'static [u8; 1] = SecurityLevel::HIGH.into(); + assert_eq!(r, &[2u8]); + let r: &'static [u8; 1] = SecurityLevel::MEDIUM.into(); + assert_eq!(r, &[3u8]); + } + + // -- Display -- + #[test] + fn test_security_level_display_matches_debug_form() { + assert_eq!(format!("{}", SecurityLevel::MASTER), "MASTER"); + assert_eq!(format!("{}", SecurityLevel::CRITICAL), "CRITICAL"); + assert_eq!(format!("{}", SecurityLevel::HIGH), "HIGH"); + assert_eq!(format!("{}", SecurityLevel::MEDIUM), "MEDIUM"); + } + + // -- Default is HIGH -- + #[test] + fn test_security_level_default_is_high() { + assert_eq!(SecurityLevel::default(), SecurityLevel::HIGH); + } + + // -- full_range, last, lowest_level, highest_level -- + #[test] + fn test_security_level_full_range() { + let r = SecurityLevel::full_range(); + assert_eq!(r.len(), 4); + assert_eq!( + r, + [ + SecurityLevel::MASTER, + SecurityLevel::CRITICAL, + SecurityLevel::HIGH, + SecurityLevel::MEDIUM, + ] + ); + } + + #[test] + fn test_security_level_last_and_lowest_are_medium() { + assert_eq!(SecurityLevel::last(), SecurityLevel::MEDIUM); + assert_eq!(SecurityLevel::lowest_level(), SecurityLevel::MEDIUM); + } + + #[test] + fn test_security_level_highest_is_master() { + assert_eq!(SecurityLevel::highest_level(), SecurityLevel::MASTER); + } + + // -- stronger_security_than: strict < -- + #[test] + fn test_stronger_security_than_master_vs_medium() { + // Master (0) is stronger than Medium (3) because 0 < 3. + assert!(SecurityLevel::MASTER.stronger_security_than(SecurityLevel::MEDIUM)); + // Medium is NOT stronger than Master. + assert!(!SecurityLevel::MEDIUM.stronger_security_than(SecurityLevel::MASTER)); + } + + #[test] + fn test_stronger_security_than_is_not_reflexive() { + // A level is not strictly stronger than itself. + assert!(!SecurityLevel::HIGH.stronger_security_than(SecurityLevel::HIGH)); + assert!(!SecurityLevel::MASTER.stronger_security_than(SecurityLevel::MASTER)); + } + + #[test] + fn test_stronger_security_than_full_matrix() { + let all = SecurityLevel::full_range(); + for (i, a) in all.iter().enumerate() { + for (j, b) in all.iter().enumerate() { + // full_range is ordered strongest -> weakest, so index i < j iff a is stronger. + assert_eq!(a.stronger_security_than(*b), i < j); + } + } + } + + // -- stronger_or_equal_security_than -- + #[test] + fn test_stronger_or_equal_security_than_reflexive() { + for lvl in SecurityLevel::full_range() { + assert!(lvl.stronger_or_equal_security_than(lvl)); + } + } + + #[test] + fn test_stronger_or_equal_security_than_strict() { + assert!(SecurityLevel::MASTER.stronger_or_equal_security_than(SecurityLevel::HIGH)); + assert!(!SecurityLevel::HIGH.stronger_or_equal_security_than(SecurityLevel::MASTER)); + } + + // -- Ordering derives -- + #[test] + fn test_security_level_ordering_master_lt_critical_lt_high_lt_medium() { + assert!(SecurityLevel::MASTER < SecurityLevel::CRITICAL); + assert!(SecurityLevel::CRITICAL < SecurityLevel::HIGH); + assert!(SecurityLevel::HIGH < SecurityLevel::MEDIUM); + } + + // -- round-trip u8 -> SecurityLevel -> u8 -- + #[test] + fn test_security_level_round_trip_u8() { + for v in 0u8..=3 { + let lvl = SecurityLevel::try_from(v).unwrap(); + assert_eq!(lvl as u8, v); + } + } +} diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs index ea064e33b65..3e437880d3e 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs @@ -241,6 +241,209 @@ mod tests { .unwrap()); } + // -- public_key_hash error paths -- + + #[test] + fn test_public_key_hash_empty_data_errors() { + use platform_value::BinaryData; + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + data: BinaryData::new(vec![]), + read_only: false, + disabled_at: None, + }; + let err = key.public_key_hash().unwrap_err(); + assert!(matches!(err, ProtocolError::EmptyPublicKeyDataError)); + } + + #[test] + fn test_public_key_hash_ecdsa_wrong_length_errors() { + use platform_value::BinaryData; + // ECDSA_SECP256K1 accepts only 33 or 65 bytes. 32 should fail with ParsingError. + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + data: BinaryData::new(vec![1u8; 32]), + read_only: false, + disabled_at: None, + }; + let err = key.public_key_hash().unwrap_err(); + match err { + ProtocolError::ParsingError(msg) => assert!(msg.contains("key length is invalid")), + other => panic!("expected ParsingError, got {:?}", other), + } + } + + #[test] + fn test_public_key_hash_bls_wrong_length_errors() { + use platform_value::BinaryData; + // BLS12_381 expects exactly 48 bytes. + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::BLS12_381, + data: BinaryData::new(vec![1u8; 40]), + read_only: false, + disabled_at: None, + }; + let err = key.public_key_hash().unwrap_err(); + match err { + ProtocolError::ParsingError(msg) => assert!(msg.contains("48 bytes for bls key")), + other => panic!("expected ParsingError, got {:?}", other), + } + } + + #[test] + fn test_public_key_hash_bls_returns_ripemd160_sha256_of_data() { + use crate::util::hash::ripemd160_sha256; + use platform_value::BinaryData; + let data = vec![7u8; 48]; + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::BLS12_381, + data: BinaryData::new(data.clone()), + read_only: false, + disabled_at: None, + }; + let hash = key + .public_key_hash() + .expect("expected hash for 48-byte bls"); + assert_eq!(hash, ripemd160_sha256(data.as_slice())); + } + + #[test] + fn test_public_key_hash_ecdsa_hash160_returns_data_itself() { + use platform_value::BinaryData; + let data = vec![9u8; 20]; + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_HASH160, + data: BinaryData::new(data.clone()), + read_only: false, + disabled_at: None, + }; + let hash = key.public_key_hash().expect("expected hash"); + assert_eq!(hash.as_slice(), data.as_slice()); + } + + #[test] + fn test_public_key_hash_bip13_script_hash_returns_data_itself() { + use platform_value::BinaryData; + let data = vec![3u8; 20]; + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::BIP13_SCRIPT_HASH, + data: BinaryData::new(data.clone()), + read_only: false, + disabled_at: None, + }; + let hash = key.public_key_hash().expect("expected hash"); + assert_eq!(hash.as_slice(), data.as_slice()); + } + + #[test] + fn test_public_key_hash_hash160_wrong_length_errors() { + use platform_value::BinaryData; + // Non-ECDSA hash variants route through Bytes20::from_vec, which should reject != 20. + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_HASH160, + data: BinaryData::new(vec![0u8; 19]), + read_only: false, + disabled_at: None, + }; + assert!(key.public_key_hash().is_err()); + } + + // -- validate_private_key_bytes: BIP13 is unsupported and always errors -- + #[test] + fn test_validate_private_key_bytes_bip13_script_hash_is_unsupported() { + use platform_value::BinaryData; + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::BIP13_SCRIPT_HASH, + data: BinaryData::new(vec![0u8; 20]), + read_only: false, + disabled_at: None, + }; + let err = key + .validate_private_key_bytes(&[0u8; 32], Network::Testnet) + .unwrap_err(); + match err { + ProtocolError::NotSupported(msg) => { + assert!(msg.contains("script hash")); + } + other => panic!("expected NotSupported, got {:?}", other), + } + } + + // -- validate_private_key_bytes for ECDSA: bad secret key bytes are handled (Ok(false)) -- + #[test] + fn test_validate_private_key_bytes_ecdsa_secret_key_parse_error_returns_false() { + use platform_value::BinaryData; + // All-zeroes is not a valid secp256k1 secret key; the code maps that + // to Ok(false) rather than Err. + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_SECP256K1, + // The actual stored public key is irrelevant here because we never get past + // the secret-key parse step. + data: BinaryData::new(vec![0u8; 33]), + read_only: false, + disabled_at: None, + }; + let ok = key + .validate_private_key_bytes(&[0u8; 32], Network::Testnet) + .unwrap(); + assert!(!ok); + } + + #[test] + fn test_validate_private_key_bytes_ecdsa_hash160_secret_key_parse_error_returns_false() { + use platform_value::BinaryData; + let key = IdentityPublicKeyV0 { + id: 0, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::HIGH, + contract_bounds: None, + key_type: KeyType::ECDSA_HASH160, + data: BinaryData::new(vec![0u8; 20]), + read_only: false, + disabled_at: None, + }; + let ok = key + .validate_private_key_bytes(&[0u8; 32], Network::Testnet) + .unwrap(); + assert!(!ok); + } + #[cfg(all(feature = "random-public-keys", feature = "ed25519-dalek"))] #[test] fn test_validate_private_key_bytes_with_random_keys_eddsa_25519_hash160() { diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 93bcabe20ef..f8e0b00a8fd 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1528,4 +1528,318 @@ mod tests { assert!(debug_str.contains("allow_signing_with_any_security_level")); assert!(debug_str.contains("allow_signing_with_any_purpose")); } + + // ----------------------------------------------------------------------- + // StateTransition enum accessor / mutator / classification tests + // + // These exercise the non-trivial match arms across the large enum, using + // the IdentityCreditTransfer, MasternodeVote, IdentityCreditWithdrawal and + // DataContractCreate variants as representative signed / unsigned / + // voting / contract cases. They intentionally do NOT use `sign`/`verify` + // (those go through BLS/ECDSA and have their own coverage elsewhere). + // ----------------------------------------------------------------------- + use crate::identity::core_script::CoreScript; + use crate::identity::{Purpose, SecurityLevel}; + use crate::prelude::Identifier; + use crate::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0; + use crate::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition; + use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0; + use crate::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition; + use crate::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0; + use crate::state_transition::masternode_vote_transition::MasternodeVoteTransition; + use crate::withdrawal::Pooling; + + fn sample_transfer_st() -> StateTransition { + let v0 = IdentityCreditTransferTransitionV0 { + identity_id: Identifier::from([1u8; 32]), + recipient_id: Identifier::from([2u8; 32]), + amount: 1_000, + nonce: 7, + user_fee_increase: 3, + signature_public_key_id: 11, + signature: BinaryData::new(vec![0u8; 65]), + }; + StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0(v0)) + } + + fn sample_masternode_vote_st() -> StateTransition { + let v0 = MasternodeVoteTransitionV0 { + pro_tx_hash: Identifier::from([3u8; 32]), + voter_identity_id: Identifier::from([4u8; 32]), + vote: Default::default(), + nonce: 2, + signature_public_key_id: 5, + signature: BinaryData::new(vec![9u8; 10]), + }; + StateTransition::MasternodeVote(MasternodeVoteTransition::V0(v0)) + } + + fn sample_withdrawal_st() -> StateTransition { + let v0 = IdentityCreditWithdrawalTransitionV0 { + identity_id: Identifier::from([5u8; 32]), + amount: 42, + core_fee_per_byte: 1, + pooling: Pooling::Never, + output_script: CoreScript::from_bytes(vec![0x76, 0xa9]), + nonce: 4, + user_fee_increase: 1, + signature_public_key_id: 3, + signature: BinaryData::new(vec![8u8; 65]), + }; + StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0(v0)) + } + + #[test] + fn test_name_returns_variant_names() { + assert_eq!(sample_transfer_st().name(), "IdentityCreditTransfer"); + assert_eq!(sample_masternode_vote_st().name(), "MasternodeVote"); + assert_eq!(sample_withdrawal_st().name(), "IdentityCreditWithdrawal"); + } + + #[test] + fn test_state_transition_type_matches_variant() { + assert_eq!( + sample_transfer_st().state_transition_type(), + StateTransitionType::IdentityCreditTransfer + ); + assert_eq!( + sample_masternode_vote_st().state_transition_type(), + StateTransitionType::MasternodeVote + ); + assert_eq!( + sample_withdrawal_st().state_transition_type(), + StateTransitionType::IdentityCreditWithdrawal + ); + } + + #[test] + fn test_is_identity_signed_excludes_asset_lock_and_shielded() { + assert!(sample_transfer_st().is_identity_signed()); + assert!(sample_masternode_vote_st().is_identity_signed()); + assert!(sample_withdrawal_st().is_identity_signed()); + } + + #[test] + fn test_signature_accessor() { + let st = sample_transfer_st(); + let sig = st.signature().expect("transfer should expose signature"); + assert_eq!(sig.len(), 65); + + let st = sample_masternode_vote_st(); + let sig = st.signature().expect("masternode vote has signature"); + assert_eq!(sig.as_slice(), &[9u8; 10]); + } + + #[test] + fn test_owner_id_accessor() { + let transfer = sample_transfer_st(); + assert_eq!(transfer.owner_id(), Some(Identifier::from([1u8; 32]))); + + let vote = sample_masternode_vote_st(); + assert_eq!(vote.owner_id(), Some(Identifier::from([4u8; 32]))); + + let withdraw = sample_withdrawal_st(); + assert_eq!(withdraw.owner_id(), Some(Identifier::from([5u8; 32]))); + } + + #[test] + fn test_signature_public_key_id_accessor() { + assert_eq!(sample_transfer_st().signature_public_key_id(), Some(11)); + assert_eq!( + sample_masternode_vote_st().signature_public_key_id(), + Some(5) + ); + assert_eq!(sample_withdrawal_st().signature_public_key_id(), Some(3)); + } + + #[test] + fn test_user_fee_increase_for_various_variants() { + // Transfer exposes its internal value. + assert_eq!(sample_transfer_st().user_fee_increase(), 3); + // Masternode vote returns 0 unconditionally. + assert_eq!(sample_masternode_vote_st().user_fee_increase(), 0); + // Withdrawal exposes its internal value. + assert_eq!(sample_withdrawal_st().user_fee_increase(), 1); + } + + #[test] + fn test_set_signature_returns_true_for_supported() { + let mut st = sample_transfer_st(); + let ok = st.set_signature(BinaryData::new(vec![0xaa; 65])); + assert!(ok); + assert_eq!(st.signature().unwrap().as_slice(), &[0xaa; 65]); + } + + #[test] + fn test_set_user_fee_increase_updates_value() { + let mut st = sample_transfer_st(); + st.set_user_fee_increase(42); + assert_eq!(st.user_fee_increase(), 42); + + // Masternode vote ignores the setter (documented no-op) — still reads 0. + let mut vote = sample_masternode_vote_st(); + vote.set_user_fee_increase(99); + assert_eq!(vote.user_fee_increase(), 0); + } + + #[test] + fn test_set_signature_public_key_id() { + let mut st = sample_transfer_st(); + st.set_signature_public_key_id(1234); + assert_eq!(st.signature_public_key_id(), Some(1234)); + } + + #[test] + fn test_required_number_of_private_keys_default() { + // Non asset-lock transitions always require 1 key. + assert_eq!(sample_transfer_st().required_number_of_private_keys(), 1); + assert_eq!( + sample_masternode_vote_st().required_number_of_private_keys(), + 1 + ); + assert_eq!(sample_withdrawal_st().required_number_of_private_keys(), 1); + } + + #[test] + fn test_inputs_none_for_legacy_variants() { + // All these variants have no PlatformAddress inputs. + assert!(sample_transfer_st().inputs().is_none()); + assert!(sample_masternode_vote_st().inputs().is_none()); + assert!(sample_withdrawal_st().inputs().is_none()); + } + + #[test] + fn test_active_version_range_legacy_transitions() { + // These all report ALL_VERSIONS per the mod.rs table. + assert_eq!(sample_transfer_st().active_version_range(), ALL_VERSIONS); + assert_eq!( + sample_masternode_vote_st().active_version_range(), + ALL_VERSIONS + ); + assert_eq!(sample_withdrawal_st().active_version_range(), ALL_VERSIONS); + } + + #[test] + fn test_unique_identifiers_non_empty() { + let ids = sample_transfer_st().unique_identifiers(); + assert_eq!(ids.len(), 1); + assert!(!ids[0].is_empty()); + } + + #[test] + fn test_required_asset_lock_balance_rejects_non_asset_lock() { + let platform_version = PlatformVersion::latest(); + let st = sample_transfer_st(); + let err = st + .required_asset_lock_balance_for_processing_start(platform_version) + .expect_err("credit transfer is not an asset lock state transition"); + match err { + ProtocolError::CorruptedCodeExecution(msg) => { + assert!( + msg.contains("is not an asset lock transaction"), + "unexpected error message: {msg}" + ); + } + other => panic!("expected CorruptedCodeExecution, got {other:?}"), + } + } + + #[test] + fn test_security_level_requirement_for_transfer() { + // IdentityCreditTransfer requires CRITICAL at TRANSFER purpose. + let st = sample_transfer_st(); + let levels = st + .security_level_requirement(Purpose::TRANSFER) + .expect("transfer state transition should return a requirement"); + assert_eq!(levels, vec![SecurityLevel::CRITICAL]); + } + + #[test] + fn test_purpose_requirement_for_transfer() { + let st = sample_transfer_st(); + let purposes = st + .purpose_requirement() + .expect("transfer state transition should have a purpose"); + assert_eq!(purposes, vec![Purpose::TRANSFER]); + } + + #[test] + fn test_optional_asset_lock_proof_none_for_transfer() { + let st = sample_transfer_st(); + assert!(st.optional_asset_lock_proof().is_none()); + } + + // ----------------------------------------------------------------------- + // Enum construction: From → StateTransition + // ----------------------------------------------------------------------- + + #[test] + fn test_from_outer_enum_into_state_transition() { + let outer: IdentityCreditTransferTransition = + IdentityCreditTransferTransition::V0(IdentityCreditTransferTransitionV0::default()); + let st: StateTransition = outer.into(); + assert!(matches!(st, StateTransition::IdentityCreditTransfer(_))); + } + + #[test] + fn test_from_masternode_vote_outer_into_state_transition() { + let outer: MasternodeVoteTransition = + MasternodeVoteTransition::V0(MasternodeVoteTransitionV0::default()); + let st: StateTransition = outer.into(); + assert!(matches!(st, StateTransition::MasternodeVote(_))); + } + + // ----------------------------------------------------------------------- + // Serialization round-trip: platform serialize / deserialize via enum. + // Exercises the top-level `StateTransition` (de)serialize glue. + // ----------------------------------------------------------------------- + + #[test] + fn test_state_transition_platform_serialize_roundtrip() { + use crate::serialization::{PlatformDeserializable, PlatformSerializable}; + let original = sample_transfer_st(); + let bytes = + PlatformSerializable::serialize_to_bytes(&original).expect("serialize should succeed"); + let restored = + StateTransition::deserialize_from_bytes(&bytes).expect("deserialize should succeed"); + assert_eq!(original, restored); + } + + #[test] + fn test_deserialize_from_bytes_in_version_succeeds_for_latest() { + use crate::serialization::PlatformSerializable; + let original = sample_transfer_st(); + let bytes = + PlatformSerializable::serialize_to_bytes(&original).expect("serialize succeeds"); + let restored = + StateTransition::deserialize_from_bytes_in_version(&bytes, PlatformVersion::latest()) + .expect("deserialize_from_bytes_in_version should succeed"); + assert_eq!(original, restored); + } + + #[test] + fn test_transaction_id_is_deterministic() { + let st = sample_transfer_st(); + let a = st.transaction_id().expect("hash should succeed"); + let b = st.transaction_id().expect("hash should succeed"); + assert_eq!(a, b); + assert_eq!(a.len(), 32); + } + + #[test] + fn test_transaction_id_changes_on_signature_change() { + let mut st = sample_transfer_st(); + let before = st.transaction_id().expect("hash should succeed"); + st.set_signature(BinaryData::new(vec![0xbb; 65])); + let after = st.transaction_id().expect("hash should succeed"); + // Different signatures produce a different serialized form. + assert_ne!(before, after); + } + + #[test] + fn test_clone_preserves_inner_state() { + let st = sample_transfer_st(); + let cloned = st.clone(); + assert_eq!(st, cloned); + } } diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 9618bb6a172..8ef12c026c6 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -2920,4 +2920,902 @@ mod tests { let result = u32_to_u16_opt(u32::MAX); assert!(result.is_err(), "value u32::MAX must not silently truncate"); } + + // --------------------------------------------------------------------- + // Length / IntoOption trait tests + // --------------------------------------------------------------------- + + #[test] + fn length_vec_option_counts_some_and_total() { + let v: Vec> = vec![Some(1), None, Some(2), None, Some(3)]; + assert_eq!(v.count(), 5); + assert_eq!(v.count_some(), 3); + + let empty: Vec> = vec![]; + assert_eq!(empty.count(), 0); + assert_eq!(empty.count_some(), 0); + } + + #[test] + fn length_option_of_length_delegates() { + let inner: Vec> = vec![Some(1), None]; + let some_inner: Option>> = Some(inner); + assert_eq!(some_inner.count(), 2); + assert_eq!(some_inner.count_some(), 1); + + let none_inner: Option>> = None; + assert_eq!(none_inner.count(), 0); + assert_eq!(none_inner.count_some(), 0); + } + + #[test] + fn length_vec_of_key_option_pair() { + let v: Vec<(u8, Option)> = vec![(1, Some(10)), (2, None), (3, Some(30)), (4, None)]; + assert_eq!(v.count(), 4); + assert_eq!(v.count_some(), 2); + } + + #[test] + fn length_btreemap_of_option() { + let mut m: BTreeMap> = BTreeMap::new(); + m.insert(1, Some(10)); + m.insert(2, None); + m.insert(3, Some(30)); + assert_eq!(m.count(), 3); + assert_eq!(m.count_some(), 2); + } + + #[test] + fn length_indexmap_of_option() { + let mut m: IndexMap> = IndexMap::new(); + m.insert(1, Some(10)); + m.insert(2, None); + m.insert(3, Some(30)); + m.insert(4, None); + assert_eq!(m.count(), 4); + assert_eq!(m.count_some(), 2); + } + + #[test] + fn into_option_returns_none_for_empty_and_some_for_nonempty() { + // Empty collection -> None + let empty: Vec> = vec![]; + assert!(empty.into_option().is_none()); + + // Non-empty, even if all are None -> Some(self) + let all_none: Vec> = vec![None, None]; + let wrapped = all_none.into_option(); + assert!(wrapped.is_some()); + assert_eq!(wrapped.unwrap().len(), 2); + + // Non-empty with some values -> Some(self) + let mixed: Vec> = vec![Some(1), None]; + assert!(mixed.into_option().is_some()); + } + + #[test] + fn into_option_for_indexmap() { + let empty: IndexMap> = IndexMap::new(); + assert!(empty.into_option().is_none()); + + let mut m: IndexMap> = IndexMap::new(); + m.insert(1, None); // only None value, but count() > 0 + let wrapped = m.into_option(); + assert!( + wrapped.is_some(), + "IntoOption must preserve maps that carry absence markers" + ); + } + + // --------------------------------------------------------------------- + // parse_key_request_type tests + // --------------------------------------------------------------------- + + #[test] + fn parse_key_request_type_missing_outer_request() { + let err = parse_key_request_type(&None) + .err() + .expect("None input must error"); + match err { + Error::RequestError { error } => { + assert!( + error.contains("missing key request type"), + "unexpected error message: {error}" + ); + } + other => panic!("expected RequestError, got: {other:?}"), + } + } + + #[test] + fn parse_key_request_type_missing_inner_request_field() { + // Outer Some, inner `request` is None -> second `ok_or` triggers. + let outer = Some(GrpcKeyType { request: None }); + let err = parse_key_request_type(&outer) + .err() + .expect("missing request must error"); + match err { + Error::RequestError { error } => { + assert!( + error.contains("empty request field"), + "unexpected error message: {error}" + ); + } + other => panic!("expected RequestError, got: {other:?}"), + } + } + + #[test] + fn parse_key_request_type_all_keys_variant() { + use dapi_grpc::platform::v0::AllKeys; + let outer = Some(GrpcKeyType { + request: Some(key_request_type::Request::AllKeys(AllKeys {})), + }); + let parsed = parse_key_request_type(&outer).unwrap(); + assert!(matches!(parsed, KeyRequestType::AllKeys)); + } + + #[test] + fn parse_key_request_type_specific_keys_variant() { + use dapi_grpc::platform::v0::SpecificKeys; + let outer = Some(GrpcKeyType { + request: Some(key_request_type::Request::SpecificKeys(SpecificKeys { + key_ids: vec![1, 2, 3], + })), + }); + let parsed = parse_key_request_type(&outer).unwrap(); + match parsed { + KeyRequestType::SpecificKeys(ids) => assert_eq!(ids, vec![1, 2, 3]), + _ => panic!("expected SpecificKeys variant"), + } + } + + #[test] + fn parse_key_request_type_search_key_rejects_invalid_kind() { + use dapi_grpc::platform::v0::{SearchKey, SecurityLevelMap}; + let mut sec_map: std::collections::HashMap = std::collections::HashMap::new(); + // 99 is not a valid GrpcKeyKind, must produce RequestError + sec_map.insert(0, 99); + + let mut purpose_map = std::collections::HashMap::new(); + purpose_map.insert( + 0u32, + SecurityLevelMap { + security_level_map: sec_map, + }, + ); + + let outer = Some(GrpcKeyType { + request: Some(key_request_type::Request::SearchKey(SearchKey { + purpose_map, + })), + }); + + let err = parse_key_request_type(&outer) + .err() + .expect("bad key kind must error"); + match err { + Error::RequestError { error } => assert!( + error.contains("missing requested key type"), + "unexpected error: {error}" + ), + other => panic!("expected RequestError for bad key kind, got: {other:?}"), + } + } + + #[test] + fn parse_key_request_type_search_key_accepts_valid_kinds() { + use dapi_grpc::platform::v0::{SearchKey, SecurityLevelMap}; + let mut sec_map: std::collections::HashMap = std::collections::HashMap::new(); + sec_map.insert(0, GrpcKeyKind::CurrentKeyOfKindRequest as i32); + sec_map.insert(1, GrpcKeyKind::AllKeysOfKindRequest as i32); + + let mut purpose_map = std::collections::HashMap::new(); + purpose_map.insert( + 0u32, + SecurityLevelMap { + security_level_map: sec_map, + }, + ); + + let outer = Some(GrpcKeyType { + request: Some(key_request_type::Request::SearchKey(SearchKey { + purpose_map, + })), + }); + + let parsed = parse_key_request_type(&outer).unwrap(); + match parsed { + KeyRequestType::SearchKey(purposes) => { + let inner = purposes.get(&0u8).expect("purpose 0 parsed"); + assert_eq!(inner.len(), 2); + assert!(matches!( + inner.get(&0u8), + Some(KeyKindRequestType::CurrentKeyOfKindRequest) + )); + assert!(matches!( + inner.get(&1u8), + Some(KeyKindRequestType::AllKeysOfKindRequest) + )); + } + _ => panic!("expected SearchKey variant"), + } + } + + // --------------------------------------------------------------------- + // FromProof error-path tests + // + // These tests verify that response/request decoding errors fire + // *before* any cryptographic proof verification is attempted, so we + // don't need a real quorum or GroveDB proof to exercise them. + // --------------------------------------------------------------------- + + /// A ContextProvider that must never be called during these tests — + /// if it is, the test has reached the cryptographic-verification stage + /// incorrectly, which is itself a meaningful failure. + struct UnreachableContextProvider; + + impl dash_context_provider::ContextProvider for UnreachableContextProvider { + fn get_data_contract( + &self, + _id: &dpp::prelude::Identifier, + _platform_version: &PlatformVersion, + ) -> Result>, dash_context_provider::ContextProviderError> + { + panic!("context provider should not be called on decode-error test") + } + + fn get_token_configuration( + &self, + _token_id: &dpp::prelude::Identifier, + ) -> Result< + Option, + dash_context_provider::ContextProviderError, + > { + panic!("context provider should not be called on decode-error test") + } + + fn get_quorum_public_key( + &self, + _quorum_type: u32, + _quorum_hash: [u8; 32], + _core_chain_locked_height: u32, + ) -> Result<[u8; 48], dash_context_provider::ContextProviderError> { + panic!("context provider should not be called on decode-error test") + } + + fn get_platform_activation_height( + &self, + ) -> Result + { + panic!("context provider should not be called on decode-error test") + } + } + + fn unreachable_provider() -> UnreachableContextProvider { + UnreachableContextProvider + } + + fn default_platform_version() -> &'static PlatformVersion { + PlatformVersion::latest() + } + + /// Build a fully-populated `GetIdentityResponse` shell so that + /// `response.proof()` and `response.metadata()` both succeed. The + /// enclosed proof is empty, so any real verification would fail — but + /// these tests stop before that point. + fn identity_response_with_proof_and_metadata() -> platform::GetIdentityResponse { + use platform::get_identity_response::{ + get_identity_response_v0::Result as V0Result, GetIdentityResponseV0, Version, + }; + platform::GetIdentityResponse { + version: Some(Version::V0(GetIdentityResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + } + } + + #[test] + fn identity_from_proof_no_proof_when_response_empty() { + // Default response has `version: None` -> response.proof() errors + // -> mapped to NoProofInResult. + let request = platform::GetIdentityRequest::default(); + let response = platform::GetIdentityResponse::default(); + + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + + assert!( + matches!(err, Error::NoProofInResult), + "expected NoProofInResult, got: {err:?}" + ); + } + + #[test] + fn identity_from_proof_empty_metadata_when_metadata_missing() { + use platform::get_identity_response::{ + get_identity_response_v0::Result as V0Result, GetIdentityResponseV0, Version, + }; + // Response has a Proof but no metadata -> EmptyResponseMetadata. + let response = platform::GetIdentityResponse { + version: Some(Version::V0(GetIdentityResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: None, + })), + }; + let request = platform::GetIdentityRequest::default(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}"); + } + + #[test] + fn identity_from_proof_empty_version_when_request_has_no_version() { + // Valid response, but request.version is None -> EmptyVersion. + let response = identity_response_with_proof_and_metadata(); + let request = platform::GetIdentityRequest { version: None }; + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::EmptyVersion), "got: {err:?}"); + } + + #[test] + fn identity_from_proof_protocol_error_on_bad_id_length() { + use dapi_grpc::platform::v0::get_identity_request::GetIdentityRequestV0; + // id must be 32 bytes; anything else fails Identifier::from_bytes. + let request: platform::GetIdentityRequest = GetIdentityRequestV0 { + id: vec![0u8; 8], + prove: true, + } + .into(); + let response = identity_response_with_proof_and_metadata(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!( + matches!(err, Error::ProtocolError { .. }), + "expected ProtocolError on bad id length, got: {err:?}" + ); + } + + /// A minimal `FromProof` impl whose `maybe_from_proof_with_metadata` + /// returns `Ok((None, ..))`, isolating the `from_proof` wrapper's + /// `None -> Error::NotFound` mapping from the decode/verify pipeline. + #[derive(Debug)] + struct MissingFromProof; + + impl FromProof<()> for MissingFromProof { + type Request = (); + type Response = (); + + fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( + _request: I, + _response: O, + _network: Network, + _platform_version: &PlatformVersion, + _provider: &'a dyn ContextProvider, + ) -> Result<(Option, ResponseMetadata, Proof), Error> + where + Self: Sized + 'a, + { + Ok((None, ResponseMetadata::default(), Proof::default())) + } + } + + #[test] + fn from_proof_maps_none_to_not_found() { + // `from_proof` (vs `maybe_from_proof`) is expected to map `Ok(None)` + // to `Error::NotFound`. Verify that wrapper behavior directly rather + // than conflating it with decode-error propagation. + let provider = unreachable_provider(); + let err = >::from_proof( + (), + (), + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!( + matches!(err, Error::NotFound), + "expected NotFound when maybe_from_proof returns None, got: {err:?}" + ); + } + + #[test] + fn identity_by_public_key_hash_invalid_length_yields_drive_error() { + use dapi_grpc::platform::v0::get_identity_by_public_key_hash_request::GetIdentityByPublicKeyHashRequestV0; + + // public_key_hash must be exactly 20 bytes; 10 bytes fails. + let request: platform::GetIdentityByPublicKeyHashRequest = + GetIdentityByPublicKeyHashRequestV0 { + public_key_hash: vec![0u8; 10], + prove: true, + } + .into(); + + // Build a response that succeeds on proof/metadata lookups. + use platform::get_identity_by_public_key_hash_response::{ + get_identity_by_public_key_hash_response_v0::Result as V0Result, + GetIdentityByPublicKeyHashResponseV0, Version, + }; + let response = platform::GetIdentityByPublicKeyHashResponse { + version: Some(Version::V0(GetIdentityByPublicKeyHashResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + + let provider = unreachable_provider(); + let err = + >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + + match err { + Error::DriveError { error } => { + assert!( + error.contains("Invalid public key hash length"), + "unexpected error body: {error}" + ); + } + other => panic!("expected DriveError, got: {other:?}"), + } + } + + #[test] + fn identity_by_non_unique_public_key_hash_rejects_bad_key_hash_length() { + use dapi_grpc::platform::v0::get_identity_by_non_unique_public_key_hash_request::GetIdentityByNonUniquePublicKeyHashRequestV0; + use platform::get_identity_by_non_unique_public_key_hash_response::{ + get_identity_by_non_unique_public_key_hash_response_v0::Result as V0Result, + GetIdentityByNonUniquePublicKeyHashResponseV0, Version, + }; + + // Build a response with a proved result so we get past the response shape check + // and hit the request validation. + let response = platform::GetIdentityByNonUniquePublicKeyHashResponse { + version: Some(Version::V0( + GetIdentityByNonUniquePublicKeyHashResponseV0 { + result: Some(V0Result::Proof( + dapi_grpc::platform::v0::get_identity_by_non_unique_public_key_hash_response::get_identity_by_non_unique_public_key_hash_response_v0::IdentityProvedResponse { + identity_proof_bytes: None, + grovedb_identity_public_key_hash_proof: Some(Proof::default()), + }, + )), + metadata: Some(ResponseMetadata::default()), + }, + )), + }; + + let request: platform::GetIdentityByNonUniquePublicKeyHashRequest = + GetIdentityByNonUniquePublicKeyHashRequestV0 { + public_key_hash: vec![0u8; 3], // must be 20 bytes + start_after: None, + prove: true, + } + .into(); + + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + + match err { + Error::RequestError { error } => { + assert!( + error.contains("Invalid public key hash length"), + "got: {error}" + ); + } + other => panic!("expected RequestError, got: {other:?}"), + } + } + + #[test] + fn identity_by_non_unique_public_key_hash_rejects_bad_start_after_length() { + use dapi_grpc::platform::v0::get_identity_by_non_unique_public_key_hash_request::GetIdentityByNonUniquePublicKeyHashRequestV0; + use platform::get_identity_by_non_unique_public_key_hash_response::{ + get_identity_by_non_unique_public_key_hash_response_v0::Result as V0Result, + GetIdentityByNonUniquePublicKeyHashResponseV0, Version, + }; + + let response = platform::GetIdentityByNonUniquePublicKeyHashResponse { + version: Some(Version::V0( + GetIdentityByNonUniquePublicKeyHashResponseV0 { + result: Some(V0Result::Proof( + dapi_grpc::platform::v0::get_identity_by_non_unique_public_key_hash_response::get_identity_by_non_unique_public_key_hash_response_v0::IdentityProvedResponse { + identity_proof_bytes: None, + grovedb_identity_public_key_hash_proof: Some(Proof::default()), + }, + )), + metadata: Some(ResponseMetadata::default()), + }, + )), + }; + + let request: platform::GetIdentityByNonUniquePublicKeyHashRequest = + GetIdentityByNonUniquePublicKeyHashRequestV0 { + public_key_hash: vec![0u8; 20], // good + start_after: Some(vec![0u8; 10]), // wrong length; must be 32 + prove: true, + } + .into(); + + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + + match err { + Error::RequestError { error } => { + assert!(error.contains("Invalid start_after length"), "got: {error}"); + } + other => panic!("expected RequestError for start_after, got: {other:?}"), + } + } + + #[test] + fn identity_by_non_unique_response_with_no_result_yields_no_proof() { + use dapi_grpc::platform::v0::get_identity_by_non_unique_public_key_hash_request::GetIdentityByNonUniquePublicKeyHashRequestV0; + use platform::get_identity_by_non_unique_public_key_hash_response::{ + GetIdentityByNonUniquePublicKeyHashResponseV0, Version, + }; + + // v0 with result=None -> NoProofInResult on the `.ok_or` branch. + let response = platform::GetIdentityByNonUniquePublicKeyHashResponse { + version: Some(Version::V0(GetIdentityByNonUniquePublicKeyHashResponseV0 { + result: None, + metadata: None, + })), + }; + let request: platform::GetIdentityByNonUniquePublicKeyHashRequest = + GetIdentityByNonUniquePublicKeyHashRequestV0 { + public_key_hash: vec![0u8; 20], + start_after: None, + prove: true, + } + .into(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::NoProofInResult), "got: {err:?}"); + } + + #[test] + fn identity_by_non_unique_response_with_no_version_yields_empty_metadata() { + // response.version = None hits the `_ => EmptyResponseMetadata` arm. + use dapi_grpc::platform::v0::get_identity_by_non_unique_public_key_hash_request::GetIdentityByNonUniquePublicKeyHashRequestV0; + let response = platform::GetIdentityByNonUniquePublicKeyHashResponse { version: None }; + let request: platform::GetIdentityByNonUniquePublicKeyHashRequest = + GetIdentityByNonUniquePublicKeyHashRequestV0 { + public_key_hash: vec![0u8; 20], + start_after: None, + prove: true, + } + .into(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::EmptyResponseMetadata), "got: {err:?}"); + } + + #[test] + fn identities_balances_rejects_non_32_byte_id() { + use dapi_grpc::platform::v0::get_identities_balances_request::GetIdentitiesBalancesRequestV0; + use platform::get_identities_balances_response::{ + get_identities_balances_response_v0::Result as V0Result, + GetIdentitiesBalancesResponseV0, Version, + }; + + let response = platform::GetIdentitiesBalancesResponse { + version: Some(Version::V0(GetIdentitiesBalancesResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + + let request: platform::GetIdentitiesBalancesRequest = GetIdentitiesBalancesRequestV0 { + ids: vec![vec![0u8; 10]], // wrong length + prove: true, + } + .into(); + + let provider = unreachable_provider(); + let err = + >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + match err { + Error::RequestError { error } => { + assert!(error.contains("all 32 bytes"), "got: {error}"); + } + other => panic!("expected RequestError, got: {other:?}"), + } + } + + #[test] + fn data_contracts_rejects_wrong_size_id() { + use dapi_grpc::platform::v0::get_data_contracts_request::GetDataContractsRequestV0; + use platform::get_data_contracts_response::{ + get_data_contracts_response_v0::Result as V0Result, GetDataContractsResponseV0, Version, + }; + + let response = platform::GetDataContractsResponse { + version: Some(Version::V0(GetDataContractsResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + let request: platform::GetDataContractsRequest = GetDataContractsRequestV0 { + ids: vec![vec![0u8; 20]], // must be 32 bytes + prove: true, + } + .into(); + let provider = unreachable_provider(); + let err = + >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + match err { + Error::RequestError { error } => { + assert!(error.contains("wrong id size"), "got: {error}"); + } + other => panic!("expected RequestError, got: {other:?}"), + } + } + + #[test] + fn upgrade_vote_status_rejects_bad_start_pro_tx_hash_length() { + use dapi_grpc::platform::v0::get_protocol_version_upgrade_vote_status_request::GetProtocolVersionUpgradeVoteStatusRequestV0; + use dapi_grpc::platform::v0::get_protocol_version_upgrade_vote_status_response::{ + get_protocol_version_upgrade_vote_status_response_v0::Result as V0Result, + GetProtocolVersionUpgradeVoteStatusResponseV0, Version, + }; + + let response = GetProtocolVersionUpgradeVoteStatusResponse { + version: Some(Version::V0(GetProtocolVersionUpgradeVoteStatusResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + // start_pro_tx_hash must be 32 bytes if non-empty. + let request: GetProtocolVersionUpgradeVoteStatusRequest = + GetProtocolVersionUpgradeVoteStatusRequestV0 { + start_pro_tx_hash: vec![0u8; 5], + count: 10, + prove: true, + } + .into(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + match err { + Error::RequestError { .. } => {} + other => panic!("expected RequestError for bad pro_tx_hash length, got: {other:?}"), + } + } + + #[test] + fn upgrade_vote_status_empty_version_on_request_none() { + use dapi_grpc::platform::v0::get_protocol_version_upgrade_vote_status_response::{ + get_protocol_version_upgrade_vote_status_response_v0::Result as V0Result, + GetProtocolVersionUpgradeVoteStatusResponseV0, Version, + }; + let response = GetProtocolVersionUpgradeVoteStatusResponse { + version: Some(Version::V0(GetProtocolVersionUpgradeVoteStatusResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + let request = GetProtocolVersionUpgradeVoteStatusRequest { version: None }; + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::EmptyVersion), "got: {err:?}"); + } + + #[test] + fn path_elements_no_proof_without_response() { + let request = GetPathElementsRequest::default(); + let response = GetPathElementsResponse::default(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::NoProofInResult), "got: {err:?}"); + } + + #[test] + fn prefunded_balance_rejects_bad_id_length() { + use dapi_grpc::platform::v0::get_prefunded_specialized_balance_request::GetPrefundedSpecializedBalanceRequestV0; + use platform::get_prefunded_specialized_balance_response::{ + get_prefunded_specialized_balance_response_v0::Result as V0Result, + GetPrefundedSpecializedBalanceResponseV0, Version, + }; + let response = platform::GetPrefundedSpecializedBalanceResponse { + version: Some(Version::V0(GetPrefundedSpecializedBalanceResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + let request: platform::GetPrefundedSpecializedBalanceRequest = + GetPrefundedSpecializedBalanceRequestV0 { + id: vec![0u8; 3], // must be 32 + prove: true, + } + .into(); + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}"); + } + + #[test] + fn epochs_info_rejects_overflowing_start_epoch() { + use dapi_grpc::platform::v0::get_epochs_info_request::GetEpochsInfoRequestV0; + use platform::get_epochs_info_response::{ + get_epochs_info_response_v0::Result as V0Result, GetEpochsInfoResponseV0, Version, + }; + let response = platform::GetEpochsInfoResponse { + version: Some(Version::V0(GetEpochsInfoResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata { + epoch: 10, + ..Default::default() + }), + })), + }; + // start_epoch > u16::MAX triggers try_u32_to_u16 error. + let request = platform::GetEpochsInfoRequest { + version: Some(platform::get_epochs_info_request::Version::V0( + GetEpochsInfoRequestV0 { + start_epoch: Some(100_000), + count: 1, + ascending: true, + prove: true, + }, + )), + }; + let provider = unreachable_provider(); + let err = + >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}"); + } + + #[test] + fn broadcast_state_transition_rejects_garbage_payload() { + // Cannot deserialize random bytes into a StateTransition, so we hit + // the ProtocolError branch before any proof work happens. + let request = platform::BroadcastStateTransitionRequest { + state_transition: vec![0xFFu8; 16], // nonsense bytes + }; + // Response structure only needs to have a valid proof field since + // deserialize happens after proof extraction. + use platform::wait_for_state_transition_result_response::{ + wait_for_state_transition_result_response_v0::Result as V0Result, Version, + WaitForStateTransitionResultResponseV0, + }; + let response = platform::WaitForStateTransitionResultResponse { + version: Some(Version::V0(WaitForStateTransitionResultResponseV0 { + result: Some(V0Result::Proof(Proof::default())), + metadata: Some(ResponseMetadata::default()), + })), + }; + let provider = unreachable_provider(); + let err = >::maybe_from_proof( + request, + response, + Network::Testnet, + default_platform_version(), + &provider, + ) + .unwrap_err(); + assert!( + matches!(err, Error::ProtocolError { .. }), + "expected ProtocolError from StateTransition decode, got: {err:?}" + ); + } }