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
151 changes: 150 additions & 1 deletion packages/rs-platform-wallet-ffi/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,20 @@ pub struct PersistenceCallbacks {
count: usize,
) -> i32,
>,
/// Per-subwallet Orchard viewing-key upserts (raw 96-byte FVK
/// encoding). Emitted once per seed-backed `bind_shielded` /
/// `shielded_add_account`; the host upserts by
/// `(wallet_id, account_index)` so later launches can rebind
/// the shielded sub-wallet without a mnemonic resolve.
#[cfg(feature = "shielded")]
pub on_persist_shielded_viewing_keys_fn: Option<
unsafe extern "C" fn(
context: *mut c_void,
wallet_id: *const u8,
entries: *const crate::shielded_persistence::ShieldedViewingKeyFFI,
count: usize,
) -> i32,
>,
/// Restore-on-load: every persisted shielded note. Host
/// allocates the array; Rust calls the matching free
/// callback after copying. Same lifetime contract as
Expand Down Expand Up @@ -464,6 +478,26 @@ pub struct PersistenceCallbacks {
count: usize,
),
>,
/// Restore-on-load: every persisted Orchard viewing key. Same
/// host-allocates / Rust-frees lifetime contract as
/// `on_load_shielded_notes_fn`. Inlined so cbindgen emits the
/// referenced struct in the header.
#[cfg(feature = "shielded")]
pub on_load_shielded_viewing_keys_fn: Option<
unsafe extern "C" fn(
context: *mut c_void,
out_entries: *mut *const crate::shielded_persistence::ShieldedViewingKeyRestoreFFI,
out_count: *mut usize,
) -> i32,
>,
#[cfg(feature = "shielded")]
pub on_load_shielded_viewing_keys_free_fn: Option<
unsafe extern "C" fn(
context: *mut c_void,
entries: *const crate::shielded_persistence::ShieldedViewingKeyRestoreFFI,
count: usize,
),
>,
/// Look up a single core transaction record by `txid` for the
/// asset-lock proof flow's persister fallback.
///
Expand Down Expand Up @@ -594,6 +628,8 @@ impl Default for PersistenceCallbacks {
#[cfg(feature = "shielded")]
on_persist_shielded_activity_fn: None,
#[cfg(feature = "shielded")]
on_persist_shielded_viewing_keys_fn: None,
#[cfg(feature = "shielded")]
on_load_shielded_notes_fn: None,
#[cfg(feature = "shielded")]
on_load_shielded_notes_free_fn: None,
Expand All @@ -609,6 +645,10 @@ impl Default for PersistenceCallbacks {
on_load_shielded_activity_fn: None,
#[cfg(feature = "shielded")]
on_load_shielded_activity_free_fn: None,
#[cfg(feature = "shielded")]
on_load_shielded_viewing_keys_fn: None,
#[cfg(feature = "shielded")]
on_load_shielded_viewing_keys_free_fn: None,
}
}
}
Expand Down Expand Up @@ -1433,7 +1473,53 @@ impl PlatformWalletPersistence for FFIPersister {
}
}

// 5) activity entries (derived activity log). The variable-
// 5) viewing keys (raw 96-byte FVK encodings). Fixed-size
// rows, no borrowed pointers. A malformed length can
// only come from a corrupted changeset; skip + warn so
// one bad row doesn't sink the flush.
if !shielded_cs.viewing_keys.is_empty() {
if let Some(cb) = self.callbacks.on_persist_shielded_viewing_keys_fn {
let entries: Vec<ShieldedViewingKeyFFI> = shielded_cs
.viewing_keys
.iter()
.filter_map(|(id, fvk)| {
let fvk_bytes: [u8; 96] = match fvk.as_slice().try_into() {
Ok(b) => b,
Err(_) => {
tracing::warn!(
fvk_len = fvk.len(),
"skipping viewing-key persist row: \
FVK is not the expected 96 bytes"
);
return None;
}
};
Some(ShieldedViewingKeyFFI {
wallet_id: id.wallet_id,
account_index: id.account_index,
fvk_bytes,
})
})
.collect();
let result = unsafe {
cb(
self.callbacks.context,
wallet_id.as_ptr(),
entries.as_ptr(),
entries.len(),
)
};
if result != 0 {
eprintln!(
"Shielded viewing-key persistence callback returned error code {}",
result
);
round_success = false;
}
}
}

// 6) activity entries (derived activity log). The variable-
// length fields (counterparty / memo / cmx + nullifier
// arrays) borrow into `backing`, a Vec of owned byte
// buffers that outlives the callback — same pointer-validity
Expand Down Expand Up @@ -2053,6 +2139,69 @@ impl PlatformWalletPersistence for FFIPersister {
}
}

// 5) persisted Orchard viewing keys (raw 96-byte FVK
// encodings), consumed by
// `PlatformWallet::bind_shielded_from_persisted` so a
// launch-time rebind needs no mnemonic resolve.
if self.callbacks.on_load_shielded_viewing_keys_fn.is_some()
!= self
.callbacks
.on_load_shielded_viewing_keys_free_fn
.is_some()
{
return Err(PersistenceError::backend(
"on_load_shielded_viewing_keys_fn and \
on_load_shielded_viewing_keys_free_fn must be provided together",
));
}
if let Some(load_viewing_keys) = self.callbacks.on_load_shielded_viewing_keys_fn {
let mut vk_ptr: *const ShieldedViewingKeyRestoreFFI = std::ptr::null();
let mut vk_count: usize = 0;
let rc = unsafe {
load_viewing_keys(self.callbacks.context, &mut vk_ptr, &mut vk_count)
};
if rc != 0 {
return Err(PersistenceError::backend(format!(
"on_load_shielded_viewing_keys_fn returned error code {}",
rc
)));
}
struct ViewingKeysGuard {
context: *mut c_void,
free_fn: Option<
unsafe extern "C" fn(
context: *mut c_void,
entries: *const ShieldedViewingKeyRestoreFFI,
count: usize,
),
>,
entries: *const ShieldedViewingKeyRestoreFFI,
count: usize,
}
impl Drop for ViewingKeysGuard {
fn drop(&mut self) {
if let Some(free_fn) = self.free_fn {
unsafe { free_fn(self.context, self.entries, self.count) };
}
}
}
let _viewing_keys_guard = ViewingKeysGuard {
context: self.callbacks.context,
free_fn: self.callbacks.on_load_shielded_viewing_keys_free_fn,
entries: vk_ptr,
count: vk_count,
};
if !vk_ptr.is_null() && vk_count > 0 {
let slice = unsafe { slice::from_raw_parts(vk_ptr, vk_count) };
for ffi in slice {
let id = SubwalletId::new(ffi.wallet_id, ffi.account_index);
shielded_state
.viewing_keys
.insert(id, ffi.fvk_bytes.to_vec());
}
}
}

out.shielded = shielded_state;
}

Expand Down
31 changes: 31 additions & 0 deletions packages/rs-platform-wallet-ffi/src/shielded_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,26 @@ pub struct ShieldedActivityFFI {
pub spent_nullifiers_count: usize,
}

/// One per-subwallet Orchard viewing key for the host to persist.
///
/// The 96 bytes are the raw `FullViewingKey` encoding (`ak ‖ nk ‖
/// rivk`); IVK / OVK / default address are all pure functions of it,
/// so this row alone lets a later launch rebind the shielded
/// sub-wallet without resolving the mnemonic. Viewing-grade only —
/// it can decrypt and recognize notes but cannot authorize a spend.
/// The host upserts one row keyed by `(wallet_id, account_index)`;
/// the FVK for a subwallet never legitimately changes on a network,
/// so a re-emit is byte-identical.
#[repr(C)]
pub struct ShieldedViewingKeyFFI {
/// 32-byte wallet identifier.
pub wallet_id: [u8; 32],
/// ZIP-32 account index.
pub account_index: u32,
/// Raw 96-byte Orchard `FullViewingKey` encoding.
pub fvk_bytes: [u8; 96],
}

// ── Restore (load) ──────────────────────────────────────────────────────

/// One persisted note as the host hands it back at boot. Mirrors
Expand Down Expand Up @@ -218,6 +238,17 @@ pub struct ShieldedSubwalletSyncStateFFI {
pub last_synced_index: u64,
}

/// One persisted Orchard viewing key as the host hands it back at
/// boot. Mirrors [`ShieldedViewingKeyFFI`] but lives in a
/// Swift-allocated array, so the buffer ownership / free contract
/// differs (see the matching `on_load_shielded_viewing_keys_free_fn`).
#[repr(C)]
pub struct ShieldedViewingKeyRestoreFFI {
pub wallet_id: [u8; 32],
pub account_index: u32,
pub fvk_bytes: [u8; 96],
}

/// One persisted activity entry as the host hands it back at boot.
/// Mirrors [`ShieldedActivityFFI`] but lives in a Swift-allocated array,
/// so the buffer ownership / free contract differs (see the matching
Expand Down
Loading
Loading