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
Original file line number Diff line number Diff line change
Expand Up @@ -6,76 +6,6 @@ use crate::util::hash::hash_double;
use crate::{identifier::Identifier, ProtocolError};
use dashcore::OutPoint;

/// Serde module for OutPoint:
/// - Human-readable (JSON): serializes as "txid:vout" string
/// - Non-human-readable (binary/platform_value): serializes as 36 bytes
mod outpoint_serde {
use dashcore::OutPoint;
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer, Serializer};
use std::str::FromStr;

pub fn serialize<S>(out_point: &OutPoint, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if serializer.is_human_readable() {
// JSON: serialize as "txid:vout" string
serializer.serialize_str(&out_point.to_string())
} else {
// Binary: serialize as 36 bytes
let bytes: [u8; 36] = (*out_point).into();
serializer.serialize_bytes(&bytes)
}
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<OutPoint, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
// JSON: deserialize from "txid:vout" string
let s = String::deserialize(deserializer)?;
OutPoint::from_str(&s).map_err(|e| D::Error::custom(format!("invalid outpoint: {}", e)))
} else {
// Binary: deserialize from 36 bytes
struct OutPointVisitor;

impl<'de> Visitor<'de> for OutPointVisitor {
type Value = OutPoint;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("36 bytes for OutPoint")
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
where
E: Error,
{
if v.len() != 36 {
return Err(E::custom(format!(
"invalid outpoint length: expected 36, got {}",
v.len()
)));
}
let mut arr = [0u8; 36];
arr.copy_from_slice(v);
Ok(OutPoint::from(arr))
}

fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_bytes(&v)
}
}

deserializer.deserialize_bytes(OutPointVisitor)
}
}
}

/// Instant Asset Lock Proof is a part of Identity Create and Identity Topup
/// transitions. It is a proof that specific output of dash is locked in credits
/// pull and the transitions can mint credits and populate identity's balance.
Expand All @@ -86,7 +16,6 @@ pub struct ChainAssetLockProof {
/// Core height on which the asset lock transaction was chain locked or higher
pub core_chain_locked_height: u32,
/// A reference to Asset Lock Special Transaction ID and output index in the payload
#[serde(with = "outpoint_serde")]
pub out_point: OutPoint,
}

Expand Down
57 changes: 57 additions & 0 deletions packages/rs-dpp/src/state_transition/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ impl StateTransition {

#[cfg(test)]
mod tests {
use hex::ToHex;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use dashcore::hashes::Hash;
use platform_value::string_encoding::Encoding;
use crate::bls::native_bls::NativeBlsModule;
use crate::data_contract::accessors::v0::DataContractV0Getters;
use crate::identity::state_transition::AssetLockProved;
use crate::identity::state_transition::asset_lock_proof::InstantAssetLockProof;
use crate::identity::accessors::IdentityGettersV0;
use crate::identity::core_script::CoreScript;
use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
Expand All @@ -31,6 +38,7 @@ mod tests {
use crate::state_transition::batch_transition::{
BatchTransition, BatchTransitionV1,
};
use crate::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0;
use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0;
use crate::state_transition::identity_create_transition::IdentityCreateTransition;
use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0;
Expand All @@ -46,12 +54,61 @@ mod tests {
use crate::version::PlatformVersion;
use crate::withdrawal::Pooling;
use crate::ProtocolError;
use dashcore::transaction::special_transaction::TransactionPayload;
use platform_version::version::LATEST_PLATFORM_VERSION;
use platform_version::TryIntoPlatformVersioned;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::BTreeMap;

#[test]
/// Given mainnet transaction 6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D,
/// when deserialized, it should be identity create transition.
fn should_build_identity_create_transition_from_mainnet_asset_lock_transaction() {
const EXPECTED_STATE_TRANSITION_HASH: &str =
"6CDCC15AC4EC68DBB414EE0DA692DFE363A996A0F285423BEFC3A29F87948A0D";
const RAW_TRANSACTION_BASE64: &str = "AwADAAAAAAAAACEDeLqSkwVyfHvYThgegiZUvPu0+dU4kyd3PJKigGLC1spBH+wrzjjA/ZGZdQmUzpQyOiC3GyP2eBp8ga9cNlnIOkptMzAtfXPA2daH3xTqt25JQ+fZ6UKB3ypzTK3fOXaAATgAAQAAAgAAIQPoVeBC6iyS0jFV0Dly5WV0SEl6uDciQqqi4EATeUJutEEfAd6+/HbUM4FLS6+lNc6AH8vaD9lViiYny4GPsl/AlBxdr0WjJxxU/B0cNVH8kRMo+W6a+1iSN+NZS7MTyzmTHwACAAEDAAAhA6S0TKbm1a/xyrYMG+Y2odspJ1roL1TcoK9h552yE1VCQSA+KpHiQ8lDBseXI/1ZCMxEvu0qopdjDojaQ4FzaZMgUGfPBeXSfMbQGksLMNseKRBLob/g0DHJWqZAxSDOuAwZAfwAIQxGIDIHY9cjWxS0tJupeJuKMZwzFKmLxkU3NmqFTcFscilVAABBH9R3vwbfA3q5XJG4m4z87OAA1uG8wup915wGGKAxdEObXPSqIvPBWrHlGTf/Uymanc2cDH1uKdsniJyoORwauPBIqlz61/Kf9HDnubX4GoHRYdnb4WzE+Tdh+L39a2dN2A==";
const EXPECTED_IDENTITY_ADDRESS: &str = "5tf2QotaJw8kRNpQEa8TXtRQ6FLxwUrY4Mtee2JF2nco";
const EXPECTED_CORE_TRANSACTION_HASH: &str =
"5529726cc14d856a363745c68ba914339c318a9b78a99bb4b4145b23d7630732";
let raw_transaction = STANDARD
.decode(RAW_TRANSACTION_BASE64)
.expect("base64 transaction should decode");
let state_transition = StateTransition::deserialize_from_bytes(&raw_transaction)
.expect("State transition deserializes correctly");

assert_eq!(
&state_transition
.transaction_id()
.expect("expected transaction id")
.encode_hex_upper::<String>(),
EXPECTED_STATE_TRANSITION_HASH
);

let StateTransition::IdentityCreate(identity_create_transition) = state_transition else {
panic!("expected identity create transition");
};

// This mainnet transaction uses a ChainAssetLockProof (not InstantAssetLockProof)
// ChainAssetLockProof doesn't embed the full transaction, just the out_point reference
let asset_lock_proof = identity_create_transition.asset_lock_proof();
let AssetLockProof::Chain(chain_proof) = asset_lock_proof else {
panic!("expected chain asset lock proof for this mainnet transaction");
};

// Verify the out_point references the expected transaction
assert_eq!(
&chain_proof.out_point.txid.to_string().to_lowercase(),
EXPECTED_CORE_TRANSACTION_HASH
);

let identity_address = identity_create_transition
.identity_id()
.to_string(Encoding::Base58);

assert_eq!(identity_address, EXPECTED_IDENTITY_ADDRESS);
}

#[test]
#[cfg(feature = "random-identities")]
fn identity_create_transition_ser_de() {
Expand Down
5 changes: 4 additions & 1 deletion packages/wasm-dpp2/tests/unit/AssetLockProof.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ describe('AssetLockProof', () => {
// Should have type field (1 = Chain)
expect(objectRepresentation.type).to.equal(1);
expect(objectRepresentation.coreChainLockedHeight).to.equal(1);
expect(objectRepresentation.outPoint).to.be.instanceOf(Uint8Array);
// outPoint is {txid, vout} object
expect(objectRepresentation.outPoint).to.be.an('object');
expect(objectRepresentation.outPoint.txid).to.exist;
expect(objectRepresentation.outPoint.vout).to.equal(1);
});
});

Expand Down
18 changes: 15 additions & 3 deletions packages/wasm-dpp2/tests/unit/ChainLockProof.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ describe('InstantLock', () => {

it('should allow to create chain lock proof from object', () => {
const outpoint = new wasm.OutPoint('e8b43025641eea4fd21190f01bd870ef90f1a8b199d8fc3376c5b62c0b1a179d', 1);

// In non-human-readable serde, txid is serialized as Uint8Array
const txidBytes = Buffer.from(outpoint.getTXID(), 'hex');

const chainlock = wasm.ChainAssetLockProof.fromObject({
coreChainLockedHeight: 11,
outPoint: outpoint.toBytes(),
outPoint: {
txid: txidBytes,
vout: outpoint.getVOUT(),
},
});

expect(chainlock.__wbg_ptr).to.not.equal(0);
expect(chainlock.coreChainLockedHeight).to.equal(11);
expect(chainlock.outPoint.getVOUT()).to.equal(1);
});

it('should round-trip via object/json/bytes', () => {
Expand All @@ -31,15 +40,18 @@ describe('InstantLock', () => {

const object = chainlock.toObject();
expect(object.coreChainLockedHeight).to.equal(11);
expect(object.outPoint).to.be.instanceOf(Uint8Array);
expect(object.outPoint).to.be.an('object');
expect(object.outPoint.txid).to.exist;
expect(object.outPoint.vout).to.equal(1);

const fromObject = wasm.ChainAssetLockProof.fromObject(object);
expect(fromObject.coreChainLockedHeight).to.equal(11);
expect(Array.from(fromObject.outPoint.toBytes())).to.deep.equal(Array.from(outpoint.toBytes()));

const json = chainlock.toJSON();
expect(json.coreChainLockedHeight).to.equal(11);
expect(json.outPoint).to.be.a('string'); // serde_json human-readable outputs base64
expect(json.outPoint).to.be.a('string');
expect(json.outPoint).to.include(':'); // "txid:vout" format

const fromJson = wasm.ChainAssetLockProof.fromJSON(json);
expect(fromJson.coreChainLockedHeight).to.equal(11);
Expand Down
Loading