From 0d95a32d0ea8c2770f73b988121a32239c6a77ab Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:36:21 +0100 Subject: [PATCH 1/2] test(dpp): verify mainnet tx 6cdc...8a0d --- .../src/state_transition/serialization.rs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/rs-dpp/src/state_transition/serialization.rs b/packages/rs-dpp/src/state_transition/serialization.rs index 998c0e017c7..e5b7d4b2544 100644 --- a/packages/rs-dpp/src/state_transition/serialization.rs +++ b/packages/rs-dpp/src/state_transition/serialization.rs @@ -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; @@ -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; @@ -46,12 +54,58 @@ 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::(), + EXPECTED_STATE_TRANSITION_HASH + ); + + let StateTransition::IdentityCreate(identity_create_transition) = state_transition else { + panic!("expected identity create transition"); + }; + + let transaction = identity_create_transition + .asset_lock_proof() + .transaction() + .expect("should have asset lock proof transaction"); + + assert_eq!( + &transaction.txid().to_hex().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() { From 711240e2aaa488795232a06b94b3dc832bb42689 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 6 Jan 2026 20:45:29 +0700 Subject: [PATCH 2/2] fix: outpoint serialization --- .../chain/chain_asset_lock_proof.rs | 71 ------------------- .../src/state_transition/serialization.rs | 13 ++-- .../tests/unit/AssetLockProof.spec.mjs | 5 +- .../tests/unit/ChainLockProof.spec.mjs | 18 ++++- 4 files changed, 27 insertions(+), 80 deletions(-) diff --git a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs index 2148f8903a2..6829ea6eaf3 100644 --- a/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs +++ b/packages/rs-dpp/src/identity/state_transition/asset_lock_proof/chain/chain_asset_lock_proof.rs @@ -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(out_point: &OutPoint, serializer: S) -> Result - 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 - 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(self, v: &[u8]) -> Result - 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(self, v: Vec) -> Result - 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. @@ -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, } diff --git a/packages/rs-dpp/src/state_transition/serialization.rs b/packages/rs-dpp/src/state_transition/serialization.rs index e5b7d4b2544..9ca5503f46e 100644 --- a/packages/rs-dpp/src/state_transition/serialization.rs +++ b/packages/rs-dpp/src/state_transition/serialization.rs @@ -89,13 +89,16 @@ mod tests { panic!("expected identity create transition"); }; - let transaction = identity_create_transition - .asset_lock_proof() - .transaction() - .expect("should have asset lock proof transaction"); + // 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!( - &transaction.txid().to_hex().to_lowercase(), + &chain_proof.out_point.txid.to_string().to_lowercase(), EXPECTED_CORE_TRANSACTION_HASH ); diff --git a/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.mjs b/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.mjs index 34ec18995e1..2c043033097 100644 --- a/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/AssetLockProof.spec.mjs @@ -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); }); }); diff --git a/packages/wasm-dpp2/tests/unit/ChainLockProof.spec.mjs b/packages/wasm-dpp2/tests/unit/ChainLockProof.spec.mjs index 0700822529a..e0f06d902b0 100644 --- a/packages/wasm-dpp2/tests/unit/ChainLockProof.spec.mjs +++ b/packages/wasm-dpp2/tests/unit/ChainLockProof.spec.mjs @@ -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', () => { @@ -31,7 +40,9 @@ 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); @@ -39,7 +50,8 @@ describe('InstantLock', () => { 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);