Skip to content
Merged
5 changes: 5 additions & 0 deletions packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,7 @@ impl PlatformWalletPersistence for FFIPersister {
nonce: entry.funds.nonce,
account_index: entry.account_index,
address_index: entry.address_index,
as_of_height: entry.funds.as_of_height,
})
.collect();
if !entries.is_empty() {
Expand Down Expand Up @@ -3409,6 +3410,10 @@ fn build_wallet_start_state(
dash_sdk::platform::address_sync::AddressFunds {
nonce: persisted.nonce,
balance: persisted.balance,
// Height pin round-trip: rows persisted before the pin
// existed load as 0 ("unknown provenance") and yield to
// the first pinned absolute — the self-healing path.
as_of_height: persisted.as_of_height,
},
);
}
Expand Down
11 changes: 11 additions & 0 deletions packages/rs-platform-wallet-ffi/src/platform_address_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ pub struct AddressBalanceEntryFFI {
pub account_index: u32,
/// DIP-17 derivation index within the account.
pub address_index: u32,
/// Platform block height `balance` is current as of — the height pin
/// (see `AddressFunds::as_of_height` in `dash-sdk`). Meaningful on the
/// persistence round-trip (persist callback → host storage → load);
/// pass 0 on request paths that only name outputs/amounts.
pub as_of_height: u64,
}

/// Parse output entries into the DPP-canonical `BTreeMap`.
Expand Down Expand Up @@ -516,6 +521,7 @@ impl From<&platform_wallet::PlatformAddressChangeSet> for PlatformAddressChangeS
nonce: entry.funds.nonce,
account_index: entry.account_index,
address_index: entry.address_index,
as_of_height: entry.funds.as_of_height,
})
.collect();

Expand Down Expand Up @@ -553,13 +559,15 @@ mod tests {
nonce: 0,
account_index: 0,
address_index: 0,
as_of_height: 0,
},
AddressBalanceEntryFFI {
address: dup,
balance: 2_000_000,
nonce: 0,
account_index: 0,
address_index: 0,
as_of_height: 0,
},
];

Expand Down Expand Up @@ -694,6 +702,7 @@ mod tests {
nonce: 0,
account_index: 0,
address_index: 0,
as_of_height: 0,
}];
assert_eq!(
unsafe { parse_outputs(out.as_ptr(), out.len()) }
Expand Down Expand Up @@ -739,6 +748,7 @@ mod tests {
nonce: 0,
account_index: 0,
address_index: 0,
as_of_height: 0,
},
AddressBalanceEntryFFI {
address: PlatformAddressFFI {
Expand All @@ -749,6 +759,7 @@ mod tests {
nonce: 0,
account_index: 0,
address_index: 0,
as_of_height: 0,
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ pub unsafe extern "C" fn platform_address_wallet_addresses_with_balances(
nonce: 0,
account_index: 0,
address_index: 0,
as_of_height: 0,
})
.collect::<Vec<_>>()
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Add the balance height pin to `platform_addresses`.
//!
//! `as_of_height` mirrors `AddressFunds::as_of_height` (see `dash-sdk`'s
//! `address_sync` module): the Platform block height a persisted balance
//! is current **as of**. It is the reconciliation rule between
//! proof-attested absolutes and the recent/compacted balance-change
//! delta stream — a delta recorded at or below the pin is already
//! included in the absolute and must not be re-applied (the ADDR-09
//! double-count).
//!
//! `DEFAULT 0` means "unknown provenance" for rows persisted before the
//! pin existed: every delta applies and any pinned absolute supersedes
//! them, which is exactly the pre-pin behavior plus self-healing.

pub fn migration() -> String {
"ALTER TABLE platform_addresses ADD COLUMN as_of_height INTEGER NOT NULL DEFAULT 0;"
.to_string()
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ pub fn apply(
if !cs.addresses.is_empty() {
let mut stmt = tx.prepare_cached(
"INSERT INTO platform_addresses \
(wallet_id, account_index, address_index, address, balance, nonce) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6) \
(wallet_id, account_index, address_index, address, balance, nonce, \
as_of_height) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) \
ON CONFLICT(wallet_id, address) DO UPDATE SET \
account_index = excluded.account_index, \
address_index = excluded.address_index, \
balance = excluded.balance, \
nonce = excluded.nonce",
nonce = excluded.nonce, \
as_of_height = excluded.as_of_height",
)?;
for entry in &cs.addresses {
// The row is keyed by the outer `wallet_id`; an entry that
Expand All @@ -51,6 +53,7 @@ pub fn apply(
entry.address.as_bytes(),
safe_cast::u64_to_i64("platform_addresses.balance", entry.funds.balance)?,
i64::from(entry.funds.nonce),
safe_cast::u64_to_i64("platform_addresses.as_of_height", entry.funds.as_of_height)?,
])?;
}
}
Expand Down Expand Up @@ -114,7 +117,7 @@ pub fn list_per_wallet(
wallet_id: &WalletId,
) -> Result<Vec<PlatformAddressRow>, WalletStorageError> {
let mut stmt = conn.prepare(
"SELECT account_index, address_index, address, balance, nonce \
"SELECT account_index, address_index, address, balance, nonce, as_of_height \
FROM platform_addresses WHERE wallet_id = ?1 \
ORDER BY account_index, address_index, address",
)?;
Expand All @@ -125,17 +128,19 @@ pub fn list_per_wallet(
row.get::<_, Vec<u8>>(2)?,
row.get::<_, i64>(3)?,
row.get::<_, i64>(4)?,
row.get::<_, i64>(5)?,
))
})?;
let mut out = Vec::new();
for r in rows {
let (account_index, address_index, address_bytes, balance, nonce) = r?;
let (account_index, address_index, address_bytes, balance, nonce, as_of_height) = r?;
out.push(decode_address_row(
account_index,
address_index,
&address_bytes,
balance,
nonce,
as_of_height,
)?);
}
Ok(out)
Expand Down Expand Up @@ -323,7 +328,8 @@ fn all_address_rows(
conn: &Connection,
) -> Result<BTreeMap<WalletId, Vec<PlatformAddressRow>>, WalletStorageError> {
let mut stmt = conn.prepare(
"SELECT wallet_id, account_index, address_index, address, balance, nonce \
"SELECT wallet_id, account_index, address_index, address, balance, nonce, \
as_of_height \
FROM platform_addresses ORDER BY wallet_id, account_index, address_index, address",
)?;
let rows = stmt.query_map([], |row| {
Expand All @@ -334,18 +340,21 @@ fn all_address_rows(
row.get::<_, Vec<u8>>(3)?,
row.get::<_, i64>(4)?,
row.get::<_, i64>(5)?,
row.get::<_, i64>(6)?,
))
})?;
let mut out: BTreeMap<WalletId, Vec<PlatformAddressRow>> = BTreeMap::new();
for r in rows {
let (wid_bytes, account_index, address_index, address_bytes, balance, nonce) = r?;
let (wid_bytes, account_index, address_index, address_bytes, balance, nonce, as_of_height) =
r?;
let wallet_id = wallet_id_from_bytes(&wid_bytes)?;
out.entry(wallet_id).or_default().push(decode_address_row(
account_index,
address_index,
&address_bytes,
balance,
nonce,
as_of_height,
)?);
}
Ok(out)
Expand All @@ -358,6 +367,7 @@ fn decode_address_row(
address_bytes: &[u8],
balance: i64,
nonce: i64,
as_of_height: i64,
) -> Result<PlatformAddressRow, WalletStorageError> {
if address_bytes.len() != 20 {
return Err(WalletStorageError::blob_decode(
Expand All @@ -367,6 +377,7 @@ fn decode_address_row(
let mut hash160 = [0u8; 20];
hash160.copy_from_slice(address_bytes);
let balance = safe_cast::i64_to_u64("platform_addresses.balance", balance)?;
let as_of_height = safe_cast::i64_to_u64("platform_addresses.as_of_height", as_of_height)?;
let nonce = u32::try_from(nonce).map_err(|_| WalletStorageError::IntegerOverflow {
field: "platform_addresses.nonce",
value: nonce as u64,
Expand All @@ -388,7 +399,11 @@ fn decode_address_row(
account_index,
address_index,
address: PlatformP2PKHAddress::new(hash160),
funds: AddressFunds { balance, nonce },
funds: AddressFunds {
balance,
nonce,
as_of_height,
},
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn entry(
funds: AddressFunds {
balance: address_index as u64 * 100,
nonce: address_index,
as_of_height: address_index as u64 * 1_000,
},
}
}
Expand Down Expand Up @@ -149,15 +150,17 @@ fn load_state_reconstructs_per_account_from_registration_and_addresses() {
account.found().get(&addr0),
Some(&AddressFunds {
balance: 0,
nonce: 0
nonce: 0,
as_of_height: 0,
}),
"address 0 funds must match the seeded entry"
);
assert_eq!(
account.found().get(&addr1),
Some(&AddressFunds {
balance: 100,
nonce: 1
nonce: 1,
as_of_height: 1_000,
}),
"address 1 funds must match the seeded entry"
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ fn tc009_platform_address_roundtrip() {
funds: AddressFunds {
nonce: 1,
balance: 500,
as_of_height: 111,
},
},
PlatformAddressBalanceEntry {
Expand All @@ -305,6 +306,7 @@ fn tc009_platform_address_roundtrip() {
funds: AddressFunds {
nonce: 2,
balance: 1500,
as_of_height: 222,
},
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ fn platform_addr_mixed_wallet_rejected() {
funds: AddressFunds {
nonce: 0,
balance: 0,
as_of_height: 0,
},
}],
..Default::default()
Expand Down
6 changes: 5 additions & 1 deletion packages/rs-platform-wallet/src/changeset/changeset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1498,7 +1498,11 @@ mod tests {
let addr1 = PlatformP2PKHAddress::new([1u8; 20]);
let addr2 = PlatformP2PKHAddress::new([2u8; 20]);

let funds = |balance, nonce| AddressFunds { balance, nonce };
let funds = |balance, nonce| AddressFunds {
balance,
nonce,
as_of_height: 0,
};
let entry = |address_index, address, funds| PlatformAddressBalanceEntry {
wallet_id,
account_index: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,19 @@ pub mod asset_lock_funding_type {
}

/// Adapter for `AddressFunds` (re-exported from `dash-sdk`; no serde
/// derive there). Encodes the two scalar fields side-by-side.
/// derive there). Encodes the scalar fields side-by-side.
pub mod address_funds {
use super::*;

#[derive(Serialize, Deserialize)]
struct Wire {
nonce: AddressNonce,
balance: Credits,
/// Height pin (see `AddressFunds::as_of_height`). Defaults to 0
/// ("unknown provenance") when decoding blobs persisted before
/// the pin existed.
#[serde(default)]
as_of_height: u64,
}

pub fn serialize<S: Serializer>(
Expand All @@ -72,6 +77,7 @@ pub mod address_funds {
Wire {
nonce: value.nonce,
balance: value.balance,
as_of_height: value.as_of_height,
}
.serialize(serializer)
}
Expand All @@ -83,6 +89,7 @@ pub mod address_funds {
Ok(AddressFunds {
nonce: w.nonce,
balance: w.balance,
as_of_height: w.as_of_height,
})
}
}
7 changes: 6 additions & 1 deletion packages/rs-platform-wallet/src/wallet/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,11 @@ mod tests {
let p2pkh2 = PlatformP2PKHAddress::new([20u8; 20]);

use dash_sdk::platform::address_sync::AddressFunds;
let funds = |balance, nonce| AddressFunds { balance, nonce };
let funds = |balance, nonce| AddressFunds {
balance,
nonce,
as_of_height: 0,
};
let wallet_id: crate::wallet::platform_wallet::WalletId = [0u8; 32];
let entry = |address_index, address, funds| crate::PlatformAddressBalanceEntry {
wallet_id,
Expand Down Expand Up @@ -1799,6 +1803,7 @@ mod tests {
funds: dash_sdk::platform::address_sync::AddressFunds {
balance: 1_000,
nonce: 0,
as_of_height: 0,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl IdentityWallet {
identity_signer: &IS,
input_address_signer: &AS,
settings: Option<PutSettings>,
) -> Result<(Identity, dash_sdk::query_types::AddressInfos), PlatformWalletError> {
) -> Result<(Identity, dash_sdk::query_types::AddressInfos, u64), PlatformWalletError> {
if inputs.is_empty() {
return Err(PlatformWalletError::InvalidIdentityData(
"At least one input address is required".to_string(),
Expand All @@ -97,7 +97,7 @@ impl IdentityWallet {
// Route through the auto-fetching SDK variant so the caller
// doesn't need to maintain its own nonce cache — Platform is
// always the source of truth at submit time.
let (mut registered_identity, address_infos) = identity
let (mut registered_identity, address_infos, proof_height) = identity
.put_with_address_funding_fetching_nonces(
&self.sdk,
inputs,
Expand Down Expand Up @@ -175,8 +175,8 @@ impl IdentityWallet {

// The spent platform-address balances are reconciled by the
// composite `PlatformWallet::register_from_addresses`, which routes
// the returned `AddressInfos` through the platform-address wallet's
// shared reconciliation seam.
Ok((identity, address_infos))
// the returned `AddressInfos` (pinned at `proof_height`) through
// the platform-address wallet's shared reconciliation seam.
Ok((identity, address_infos, proof_height))
}
}
Loading
Loading