Skip to content
Closed
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
46 changes: 46 additions & 0 deletions packages/rs-dpp/src/serialization/serialization_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,52 @@ pub trait PlatformDeserializableUntrusted {
Self: Sized;
}

/// [`PlatformDeserializableUntrusted`] that also refuses bytes left over after the value.
///
/// The derived untrusted decoders discard how many bytes they consumed, so any suffix after a
/// valid value is silently ignored. A caller that shows a user what the bytes contain and then
/// signs them must know the value is all there is.
///
/// Decodes with the same configuration the derive emits for `unversioned` types without `into`,
/// bounded by [`UNTRUSTED_EXACT_DECODE_LIMIT`] (the `StateTransition` wire limit) because the
/// input is untrusted.
pub trait PlatformDeserializableUntrustedExact: Sized {
fn deserialize_from_bytes_untrusted_exact(data: &[u8]) -> Result<Self, ProtocolError>;
}

/// Byte budget of [`PlatformDeserializableUntrustedExact`]; the `StateTransition` wire limit.
pub const UNTRUSTED_EXACT_DECODE_LIMIT: usize = 100_000;

impl<T: bincode::DecodeUntrusted<()>> PlatformDeserializableUntrustedExact for T {
fn deserialize_from_bytes_untrusted_exact(data: &[u8]) -> Result<Self, ProtocolError> {
let config = bincode::config::standard()
.with_big_endian()
.with_limit::<UNTRUSTED_EXACT_DECODE_LIMIT>();
let (value, consumed) =
bincode::decode_from_slice_untrusted(data, config).map_err(|e| match e {
bincode::error::DecodeError::Io { .. }
| bincode::error::DecodeError::LimitExceeded => {
ProtocolError::MaxEncodedBytesReachedError {
max_size_kbytes: UNTRUSTED_EXACT_DECODE_LIMIT,
size_hit: data.len(),
}
}
_ => ProtocolError::PlatformDeserializationError(format!(
"unable to deserialize {}: {e}",
std::any::type_name::<T>()
)),
})?;
if consumed != data.len() {
return Err(ProtocolError::PlatformDeserializationError(format!(
"unable to deserialize {}: {} bytes left over after the value",
std::any::type_name::<T>(),
data.len() - consumed
)));
}
Ok(value)
}
}

/// We will deserialize a versioned structure into a code structure
/// For example we have DataContractV0 and DataContractV1
/// The system version will tell which version to deserialize into
Expand Down
53 changes: 53 additions & 0 deletions packages/rs-dpp/src/state_transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,59 @@ mod json_convertible_tests {
"shieldFromIdentity",
);
}

/// Every kind decodes from its untagged bytes (the inner transition serialized on its own)
/// into the same transition, and from its tagged bytes exactly.
#[test]
fn every_kind_decodes_untagged_and_exactly() {
use crate::serialization::{PlatformDeserializableUntrustedExact, PlatformSerializable};

let transitions = [
StateTransition::DataContractCreate(crate::state_transition::data_contract_create_transition::json_convertible_tests::fixture()),
StateTransition::DataContractUpdate(crate::state_transition::data_contract_update_transition::json_convertible_tests::fixture()),
StateTransition::Batch(crate::state_transition::batch_transition::json_convertible_tests::fixture()),
StateTransition::IdentityCreate(crate::state_transition::identity_create_transition::json_convertible_tests::fixture()),
StateTransition::IdentityTopUp(crate::state_transition::identity_topup_transition::json_convertible_tests::fixture()),
StateTransition::IdentityCreditWithdrawal(crate::state_transition::identity_credit_withdrawal_transition::json_convertible_tests::fixture()),
StateTransition::IdentityUpdate(crate::state_transition::identity_update_transition::json_convertible_tests::fixture()),
StateTransition::IdentityKeyLimitsUpdate(crate::state_transition::identity_key_limits_update_transition::json_convertible_tests::fixture()),
StateTransition::ContractUserModeration(crate::state_transition::contract_user_moderation_transition::json_convertible_tests::fixture()),
StateTransition::ContractFeeClaim(crate::state_transition::contract_fee_claim_transition::json_convertible_tests::fixture()),
StateTransition::IdentityCreditTransfer(crate::state_transition::identity_credit_transfer_transition::json_convertible_tests::fixture()),
StateTransition::MasternodeVote(crate::state_transition::masternode_vote_transition::json_convertible_tests::fixture()),
StateTransition::IdentityCreditTransferToAddresses(crate::state_transition::identity_credit_transfer_to_addresses_transition::json_convertible_tests::fixture()),
StateTransition::IdentityCreateFromAddresses(crate::state_transition::identity_create_from_addresses_transition::json_convertible_tests::fixture()),
StateTransition::IdentityTopUpFromAddresses(crate::state_transition::identity_topup_from_addresses_transition::json_convertible_tests::fixture()),
StateTransition::AddressFundsTransfer(crate::state_transition::address_funds_transfer_transition::json_convertible_tests::fixture()),
StateTransition::AddressFundingFromAssetLock(crate::state_transition::address_funding_from_asset_lock_transition::json_convertible_tests::fixture()),
StateTransition::AddressCreditWithdrawal(crate::state_transition::address_credit_withdrawal_transition::json_convertible_tests::fixture()),
StateTransition::Shield(crate::state_transition::shield_transition::json_convertible_tests::fixture()),
StateTransition::ShieldedTransfer(crate::state_transition::shielded_transfer_transition::json_convertible_tests::fixture()),
StateTransition::Unshield(crate::state_transition::unshield_transition::json_convertible_tests::fixture()),
StateTransition::ShieldFromAssetLock(crate::state_transition::shield_from_asset_lock_transition::json_convertible_tests::fixture()),
StateTransition::ShieldedWithdrawal(crate::state_transition::shielded_withdrawal_transition::json_convertible_tests::fixture()),
StateTransition::IdentityCreateFromShieldedPool(crate::state_transition::identity_create_from_shielded_pool_transition::json_convertible_tests::fixture()),
StateTransition::IdentityTopUpFromShieldedPool(crate::state_transition::identity_top_up_from_shielded_pool_transition::json_convertible_tests::fixture()),
StateTransition::ShieldFromIdentity(crate::state_transition::shield_from_identity_transition::json_convertible_tests::fixture()),
];
for transition in transitions {
let tagged = transition.serialize_to_bytes().expect("serializes");
assert_eq!(
StateTransition::deserialize_from_bytes_untrusted_exact(&tagged).expect("tagged"),
transition
);
// The variant tags are all below 251, so bincode's varint writes them in one byte.
let untagged = &tagged[1..];
assert_eq!(
StateTransition::deserialize_untagged_untrusted_exact(
transition.state_transition_type(),
untagged
)
.unwrap_or_else(|e| panic!("{} untagged: {e}", transition.name())),
transition
);
}
}
}

impl OptionallyAssetLockProved for StateTransition {
Expand Down
257 changes: 254 additions & 3 deletions packages/rs-dpp/src/state_transition/serialization.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::serialization::PlatformDeserializableUntrusted;
use crate::state_transition::StateTransition;
use crate::serialization::{PlatformDeserializableUntrusted, PlatformDeserializableUntrustedExact};
use crate::state_transition::*;
use crate::ProtocolError;

impl StateTransition {
Expand All @@ -13,6 +13,125 @@ impl StateTransition {
})
.collect()
}

/// Decodes one transition of `state_transition_type` serialized on its own, without the
/// `StateTransition` variant tag in front (what `IdentityUpdateTransition::serialize_to_bytes`
/// produces, for example). Bytes left over after the transition are refused.
pub fn deserialize_untagged_untrusted_exact(
state_transition_type: StateTransitionType,
bytes: &[u8],
) -> Result<Self, ProtocolError> {
let state_transition: Self = match state_transition_type {
StateTransitionType::DataContractCreate => {
DataContractCreateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::DataContractUpdate => {
DataContractUpdateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::Batch => {
BatchTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityCreate => {
IdentityCreateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityTopUp => {
IdentityTopUpTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityCreditWithdrawal => {
IdentityCreditWithdrawalTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::IdentityUpdate => {
IdentityUpdateTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityCreditTransfer => {
IdentityCreditTransferTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::MasternodeVote => {
MasternodeVoteTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityCreditTransferToAddresses => {
IdentityCreditTransferToAddressesTransition::deserialize_from_bytes_untrusted_exact(
bytes,
)?
.into()
}
StateTransitionType::IdentityCreateFromAddresses => {
IdentityCreateFromAddressesTransition::deserialize_from_bytes_untrusted_exact(
bytes,
)?
.into()
}
StateTransitionType::IdentityTopUpFromAddresses => {
IdentityTopUpFromAddressesTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::AddressFundsTransfer => {
AddressFundsTransferTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::AddressFundingFromAssetLock => {
AddressFundingFromAssetLockTransition::deserialize_from_bytes_untrusted_exact(
bytes,
)?
.into()
}
StateTransitionType::AddressCreditWithdrawal => {
AddressCreditWithdrawalTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::Shield => {
ShieldTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::ShieldedTransfer => {
ShieldedTransferTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::Unshield => {
UnshieldTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::ShieldFromAssetLock => {
ShieldFromAssetLockTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::ShieldedWithdrawal => {
ShieldedWithdrawalTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityCreateFromShieldedPool => {
IdentityCreateFromShieldedPoolTransition::deserialize_from_bytes_untrusted_exact(
bytes,
)?
.into()
}
StateTransitionType::ShieldFromIdentity => {
ShieldFromIdentityTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
StateTransitionType::IdentityTopUpFromShieldedPool => {
IdentityTopUpFromShieldedPoolTransition::deserialize_from_bytes_untrusted_exact(
bytes,
)?
.into()
}
StateTransitionType::IdentityKeyLimitsUpdate => {
IdentityKeyLimitsUpdateTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::ContractUserModeration => {
ContractUserModerationTransition::deserialize_from_bytes_untrusted_exact(bytes)?
.into()
}
StateTransitionType::ContractFeeClaim => {
ContractFeeClaimTransition::deserialize_from_bytes_untrusted_exact(bytes)?.into()
}
};
// Every arm converts into `StateTransition`, so a mismatched arm would still compile.
if state_transition.state_transition_type() != state_transition_type {
return Err(ProtocolError::CorruptedCodeExecution(format!(
"untagged {state_transition_type} bytes decoded as {}",
state_transition.state_transition_type()
)));
}
Ok(state_transition)
}
}

#[cfg(test)]
Expand All @@ -32,7 +151,9 @@ mod tests {
use crate::prelude::AssetLockProof;
use crate::serialization::PlatformMessageSignable;
use crate::serialization::Signable;
use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
use crate::serialization::{
PlatformDeserializableUntrusted, PlatformDeserializableUntrustedExact, PlatformSerializable,
};
use crate::state_transition::data_contract_create_transition::DataContractCreateTransition;
use crate::state_transition::data_contract_update_transition::{
DataContractUpdateTransition, DataContractUpdateTransitionV0,
Expand All @@ -56,6 +177,8 @@ mod tests {
use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0;
use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0;
use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0;
use crate::state_transition::identity_update_transition::IdentityUpdateTransition;
use crate::state_transition::StateTransitionType;
use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
use crate::state_transition::StateTransition;
use crate::tests::fixtures::{
Expand Down Expand Up @@ -770,4 +893,132 @@ mod tests {
assert_eq!(recovered[1], st2);
assert_eq!(recovered[2], st3);
}

#[test]
#[cfg(feature = "random-identities")]
fn exact_decode_refuses_trailing_bytes() {
let platform_version = PlatformVersion::latest();
let identity = Identity::random_identity(5, Some(5), platform_version)
.expect("expected a random identity");
let st: StateTransition = IdentityCreditWithdrawalTransitionV0 {
identity_id: identity.id(),
amount: 1000000,
core_fee_per_byte: 34,
pooling: Pooling::Standard,
output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
nonce: 1,
user_fee_increase: 0,
signature_public_key_id: 0,
signature: [1u8; 65].to_vec().into(),
}
.into();
let bytes = st.serialize_to_bytes().unwrap();

assert_eq!(
StateTransition::deserialize_from_bytes_untrusted_exact(&bytes).unwrap(),
st
);

let mut padded = bytes.clone();
padded.push(0);
// The derived decoder ignores the suffix; the exact one refuses it.
assert_eq!(
StateTransition::deserialize_from_bytes_untrusted(&padded).unwrap(),
st
);
assert!(matches!(
StateTransition::deserialize_from_bytes_untrusted_exact(&padded),
Err(ProtocolError::PlatformDeserializationError(message))
if message.contains("1 bytes left over")
));
}

/// A tagged transition followed by another decodes loosely as the first alone; only the
/// exact decoder reports the suffix.
#[test]
fn exact_decode_refuses_a_transition_followed_by_another() {
let update: StateTransition = IdentityUpdateTransitionV0 {
identity_id: Identifier::from([0x21; 32]),
revision: 1,
nonce: 1,
disable_public_keys: vec![1],
..Default::default()
}
.into();
let withdrawal: StateTransition = IdentityCreditWithdrawalTransitionV0 {
identity_id: Identifier::from([0x21; 32]),
amount: 1000000,
core_fee_per_byte: 34,
pooling: Pooling::Standard,
output_script: CoreScript::from_bytes((0..23).collect::<Vec<u8>>()),
nonce: 2,
user_fee_increase: 0,
signature_public_key_id: 0,
signature: Default::default(),
}
.into();

let mut both = update.serialize_to_bytes().unwrap();
both.extend_from_slice(&withdrawal.serialize_to_bytes().unwrap());

assert_eq!(
StateTransition::deserialize_from_bytes_untrusted(&both).unwrap(),
update
);
assert!(matches!(
StateTransition::deserialize_from_bytes_untrusted_exact(&both),
Err(ProtocolError::PlatformDeserializationError(message)) if message.contains("left over")
));
}

#[test]
#[cfg(feature = "random-identities")]
fn untagged_decode_matches_the_tagged_transition() {
let platform_version = PlatformVersion::latest();
let identity = Identity::random_identity(5, Some(5), platform_version)
.expect("expected a random identity");
let update = IdentityUpdateTransitionV0 {
signature: [7u8; 65].to_vec().into(),
signature_public_key_id: 0,
identity_id: identity.id(),
revision: 1,
nonce: 1,
add_public_keys: identity
.public_keys()
.values()
.map(|public_key| public_key.into())
.collect(),
disable_public_keys: vec![],
user_fee_increase: 0,
};
let inner_bytes = IdentityUpdateTransition::from(update.clone())
.serialize_to_bytes()
.unwrap();
let st: StateTransition = update.into();

let decoded = StateTransition::deserialize_untagged_untrusted_exact(
StateTransitionType::IdentityUpdate,
&inner_bytes,
)
.expect("untagged identity update decodes");
assert_eq!(decoded, st);
assert_eq!(
&decoded.serialize_to_bytes().unwrap()[1..],
&inner_bytes[..]
);

assert!(StateTransition::deserialize_untagged_untrusted_exact(
StateTransitionType::Batch,
&inner_bytes,
)
.is_err());

let mut padded = inner_bytes.clone();
padded.push(0);
assert!(StateTransition::deserialize_untagged_untrusted_exact(
StateTransitionType::IdentityUpdate,
&padded,
)
.is_err());
}
}
Loading
Loading