Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions packages/rs-dpp/src/identity/conversion/platform_value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,136 @@ impl TryFromPlatformVersioned<&Value> for Identity {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::identity::accessors::IdentityGettersV0;
use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
use crate::identity::IdentityPublicKey;
use crate::identity::{KeyType, Purpose, SecurityLevel};
use crate::serialization::ValueConvertible;
use platform_value::{platform_value, BinaryData, Identifier};
use platform_version::version::LATEST_PLATFORM_VERSION;
use std::collections::BTreeMap;

fn sample_identity_v0() -> IdentityV0 {
let mut keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
keys.insert(
0,
IdentityPublicKey::V0(IdentityPublicKeyV0 {
id: 0,
purpose: Purpose::AUTHENTICATION,
security_level: SecurityLevel::MASTER,
contract_bounds: None,
key_type: KeyType::ECDSA_SECP256K1,
read_only: false,
data: BinaryData::new(vec![0x01; 33]),
disabled_at: None,
}),
);
IdentityV0 {
id: Identifier::from([42u8; 32]),
public_keys: keys,
balance: 7,
revision: 2,
}
}

// A `platform_value::Value` that `try_from_platform_versioned` accepts and
// deserializes into an `IdentityV0` via `platform_value::from_value::<IdentityV0>`.
//
// NOTE: this is *not* a byte-for-byte mirror of `IdentityV0::to_object()`.
// `to_object()` produces `Value::Bytes` for `BinaryData` fields (e.g. the
// public-key `data`), while this fixture encodes `data` as a base64 STRING.
// Both shapes round-trip through the serde deserializer because the inner
// `platform_value` deserializer behaves as `is_human_readable() = true` for
// nested fields, which accepts the base64-string representation of
// `BinaryData`. This fixture deliberately exercises the human-readable path.
//
// Each inner public key carries the adjacency-tag `$formatVersion: "0"` that
// `IdentityPublicKey`'s serde enum representation requires.
//
// frozen: V0 consensus behavior.
fn tagged_raw_value() -> Value {
use platform_value::string_encoding::{encode, Encoding};
let data_b64 = encode(&[0x22u8; 33], Encoding::Base64);
platform_value!({
"id": Identifier::from([7u8; 32]),
"publicKeys": [
{
"$formatVersion": "0",
"id": 0u32,
"type": 0u8,
"purpose": 0u8,
"securityLevel": 0u8,
"contractBounds": Value::Null,
"data": data_b64,
"readOnly": false,
"disabledAt": Value::Null,
}
],
"balance": 100u64,
"revision": 1u64,
})
}

#[test]
fn try_from_platform_versioned_owned_value_parses_legacy_shape() {
let value = tagged_raw_value();
let identity = Identity::try_from_platform_versioned(value, LATEST_PLATFORM_VERSION)
.expect("should parse legacy raw object");
assert_eq!(identity.balance(), 100);
assert_eq!(identity.revision(), 1);
assert_eq!(identity.public_keys().len(), 1);
}

#[test]
fn try_from_platform_versioned_ref_value_parses_legacy_shape() {
let value = tagged_raw_value();
let identity = Identity::try_from_platform_versioned(&value, LATEST_PLATFORM_VERSION)
.expect("should parse legacy raw object from &Value");
assert_eq!(identity.balance(), 100);
}

#[test]
fn try_from_platform_versioned_errors_on_garbage_owned() {
let value = Value::Null;
let result = Identity::try_from_platform_versioned(value, LATEST_PLATFORM_VERSION);
assert!(matches!(result, Err(ProtocolError::ValueError(_))));
}

#[test]
fn try_from_platform_versioned_errors_on_garbage_ref() {
let value = Value::Text("not a map".to_string());
let result = Identity::try_from_platform_versioned(&value, LATEST_PLATFORM_VERSION);
assert!(matches!(result, Err(ProtocolError::ValueError(_))));
}

// to_cleaned_object on the Identity enum wrapper uses the default body
// (`self.to_object()`), inherited through `IdentityPlatformValueConversionMethodsV0`.
// to_object itself comes from the `ValueConvertible` derive on Identity, which
// produces the tagged `$formatVersion: "0"` form.
#[test]
fn identity_wrapper_to_cleaned_object_includes_format_version_tag() {
let identity: Identity = sample_identity_v0().into();
let value = identity.to_cleaned_object().expect("to_cleaned_object");
let map = value.to_map_ref().expect("map");
assert!(
map.iter()
.any(|(k, _)| k.as_text() == Some("$formatVersion")),
"Identity enum wrapper must keep its format version tag"
);
}

#[test]
fn identity_wrapper_to_object_differs_from_v0_inner_shape() {
// Sanity check: the Identity wrapper's to_object includes `$formatVersion`,
// whereas IdentityV0's own to_object is a flat map.
let v0 = sample_identity_v0();
let wrapper: Identity = v0.clone().into();
let inner_value = v0.to_object().unwrap();
let outer_value = wrapper.to_object().unwrap();
assert_ne!(inner_value, outer_value);
}
}
175 changes: 175 additions & 0 deletions packages/rs-dpp/src/identity/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,178 @@ impl Identity {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::identity::accessors::IdentityGettersV0;
use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
use crate::identity::{KeyType, Purpose, SecurityLevel};
use platform_value::{BinaryData, Identifier};
use platform_version::version::LATEST_PLATFORM_VERSION;
use std::collections::BTreeMap;

fn sample_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: BinaryData::new(vec![0x42; 33]),
disabled_at: None,
})
}

#[test]
fn default_versioned_returns_default_v0() {
let identity =
Identity::default_versioned(LATEST_PLATFORM_VERSION).expect("default should succeed");
assert_eq!(identity.id(), Identifier::default());
assert_eq!(identity.balance(), 0);
assert_eq!(identity.revision(), 0);
assert!(identity.public_keys().is_empty());
}

#[test]
fn new_with_id_and_keys_preserves_inputs() {
let id = Identifier::from([4u8; 32]);
let mut keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
keys.insert(0, sample_key(0));
keys.insert(1, sample_key(1));

let identity = Identity::new_with_id_and_keys(id, keys.clone(), LATEST_PLATFORM_VERSION)
.expect("new_with_id_and_keys");
assert_eq!(identity.id(), id);
assert_eq!(identity.balance(), 0);
assert_eq!(identity.revision(), 0);
assert_eq!(identity.public_keys().len(), 2);
}

#[test]
fn into_partial_identity_info_preserves_balance_and_revision() {
let mut keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();
keys.insert(0, sample_key(0));
let v0 = IdentityV0 {
id: Identifier::from([5u8; 32]),
public_keys: keys,
balance: 123,
revision: 7,
};
let identity: Identity = v0.clone().into();
let partial = identity.into_partial_identity_info();
assert_eq!(partial.id, v0.id);
assert_eq!(partial.balance, Some(123));
assert_eq!(partial.revision, Some(7));
assert_eq!(partial.loaded_public_keys.len(), 1);
assert!(partial.not_found_public_keys.is_empty());
}

#[test]
fn into_partial_identity_info_no_balance_drops_balance() {
let v0 = IdentityV0 {
id: Identifier::from([6u8; 32]),
public_keys: BTreeMap::new(),
balance: 999,
revision: 2,
};
let identity: Identity = v0.into();
let partial = identity.into_partial_identity_info_no_balance();
assert!(partial.balance.is_none());
assert_eq!(partial.revision, Some(2));
}

#[test]
fn from_v0_conversion_works() {
let v0 = IdentityV0 {
id: Identifier::from([1u8; 32]),
public_keys: BTreeMap::new(),
balance: 1,
revision: 1,
};
let identity: Identity = v0.clone().into();
match identity {
Identity::V0(inner) => assert_eq!(inner, v0),
}
}

#[test]
fn clone_and_equality() {
let id = Identifier::from([3u8; 32]);
let identity =
Identity::new_with_id_and_keys(id, BTreeMap::new(), LATEST_PLATFORM_VERSION).unwrap();
let clone = identity.clone();
assert_eq!(identity, clone);
}

#[cfg(feature = "identity-hashing")]
#[test]
fn hash_is_stable_for_same_identity() {
let id = Identifier::from([8u8; 32]);
let identity =
Identity::new_with_id_and_keys(id, BTreeMap::new(), LATEST_PLATFORM_VERSION).unwrap();
let h1 = identity.hash().unwrap();
let h2 = identity.hash().unwrap();
assert_eq!(h1, h2);
// The hash is a fixed-size SHA256-double, 32 bytes.
assert_eq!(h1.len(), 32);
}

#[cfg(feature = "identity-hashing")]
#[test]
fn hash_differs_for_different_identities() {
let a = Identity::new_with_id_and_keys(
Identifier::from([0u8; 32]),
BTreeMap::new(),
LATEST_PLATFORM_VERSION,
)
.unwrap();
let b = Identity::new_with_id_and_keys(
Identifier::from([1u8; 32]),
BTreeMap::new(),
LATEST_PLATFORM_VERSION,
)
.unwrap();
assert_ne!(a.hash().unwrap(), b.hash().unwrap());
}

#[cfg(feature = "state-transitions")]
#[test]
fn new_with_input_addresses_and_keys_is_deterministic() {
use crate::address_funds::PlatformAddress;

let mut inputs: BTreeMap<PlatformAddress, (u32, u64)> = BTreeMap::new();
inputs.insert(PlatformAddress::P2pkh([0x11; 20]), (1, 0));
inputs.insert(PlatformAddress::P2pkh([0x22; 20]), (2, 0));

let keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();

let a = Identity::new_with_input_addresses_and_keys(
&inputs,
keys.clone(),
LATEST_PLATFORM_VERSION,
)
.unwrap();
let b = Identity::new_with_input_addresses_and_keys(
&inputs,
keys.clone(),
LATEST_PLATFORM_VERSION,
)
.unwrap();
// Deterministic derivation: same inputs -> same id.
assert_eq!(a.id(), b.id());
}

#[cfg(feature = "state-transitions")]
#[test]
fn new_with_input_addresses_and_keys_fails_on_empty_inputs() {
use crate::address_funds::PlatformAddress;
let inputs: BTreeMap<PlatformAddress, (u32, u64)> = BTreeMap::new();
let keys: BTreeMap<u32, IdentityPublicKey> = BTreeMap::new();

let result =
Identity::new_with_input_addresses_and_keys(&inputs, keys, LATEST_PLATFORM_VERSION);
assert!(result.is_err());
}
}
Loading
Loading