Repository navigation
feat(platform-wallet)!: report a freshly built asset lock's outpoint to the caller #5476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
13509f3
feat(platform-wallet)!: report a freshly built asset lock's outpoint …
jeanpierreroma 3fab1ac
feat(swift-sdk)!: hand the built-lock handler a typed outpoint
jeanpierreroma 4b0f4b4
test(swift-sdk): cover the built-lock callback bridge and outpoint de…
jeanpierreroma 1e5c740
Merge branch 'v5.1-dev' into feat/asset-lock-built-observer
jeanpierreroma 9c7706f
feat(swift-sdk): let consumers build an AssetLockBuiltOutPoint
jeanpierreroma 536f854
Merge remote-tracking branch 'origin/v5.1-dev' into feat/asset-lock-b…
jeanpierreroma b6d65c7
docs(platform-wallet): describe the built-lock observer on the wallet…
jeanpierreroma 42c8c72
test(platform-wallet): cover the built-lock observer on the wallet-dr…
jeanpierreroma 3e5d167
test(platform-wallet): drop a no-op drop in the wallet-drain observer…
jeanpierreroma e3e4f33
test(platform-wallet): pin the asset-lock observer wiring through eve…
jeanpierreroma 39f9d29
Merge remote-tracking branch 'origin/v5.1-dev' into feat/asset-lock-b…
romchornyi 0aac302
docs(unified-sdk-jni): say Kotlin hosts do not take the built-lock ca…
romchornyi a063b37
refactor(platform-wallet): keep the built-lock observer with the asse…
romchornyi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
109 changes: 109 additions & 0 deletions
109
packages/rs-platform-wallet-ffi/src/asset_lock/built_callback.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| //! C callback that reports the outpoint of a newly built asset lock. | ||
|
|
||
| use std::os::raw::c_void; | ||
|
|
||
| use dashcore::OutPoint; | ||
| use platform_wallet::wallet::asset_lock::AssetLockBuiltObserver; | ||
|
|
||
| use crate::asset_lock_persistence::outpoint_to_bytes; | ||
|
|
||
| /// Receives the outpoint of an asset lock that a funding call has just | ||
| /// built, before the lock's row is persisted or the lock is broadcast. | ||
| /// | ||
| /// `out_point_36` points to 36 bytes (32-byte raw txid followed by the | ||
| /// 4-byte little-endian vout, the layout of `AssetLockEntryFFI.out_point`) | ||
| /// and is valid only for the duration of the call. The callback runs | ||
| /// synchronously on the funding call's worker thread, at most once per | ||
| /// call, and must return promptly without calling back into the wallet. | ||
| /// Record the outpoint before returning: the lock's row is persisted right | ||
| /// after. The funding call's result then tells what happened to the lock: | ||
| /// `ErrorTransactionBroadcastRejected` (26) means it was removed again; any | ||
| /// other failure leaves it tracked and resumable by this outpoint. | ||
| /// `NULL` means no callback. | ||
| pub type AssetLockBuiltFn = | ||
| Option<unsafe extern "C" fn(context: *mut c_void, out_point_36: *const u8)>; | ||
|
|
||
| /// A caller-supplied [`AssetLockBuiltFn`] with its opaque context. | ||
| /// | ||
| /// The context is held as `usize` so the funding future that captures it | ||
| /// stays `Send + 'static`; the caller's contract keeps it valid for the | ||
| /// whole (blocking) FFI call. | ||
| #[derive(Clone, Copy)] | ||
| pub(crate) struct AssetLockBuiltCallback { | ||
| func: unsafe extern "C" fn(context: *mut c_void, out_point_36: *const u8), | ||
| context: usize, | ||
| } | ||
|
|
||
| impl AssetLockBuiltCallback { | ||
| /// `None` when the caller passed no callback. | ||
| pub(crate) fn new(func: AssetLockBuiltFn, context: *mut c_void) -> Option<Self> { | ||
| func.map(|func| Self { | ||
| func, | ||
| context: context as usize, | ||
| }) | ||
| } | ||
|
|
||
| /// The callback as the observer the funding paths take. | ||
| pub(crate) fn into_observer(self) -> impl Fn(OutPoint) + Send + Sync { | ||
| move |out_point| self.notify(out_point) | ||
| } | ||
|
|
||
| fn notify(&self, out_point: OutPoint) { | ||
| let bytes = outpoint_to_bytes(&out_point); | ||
| // SAFETY: the caller guarantees `func` and `context` stay valid for | ||
| // the duration of the FFI call this callback belongs to, and `bytes` | ||
| // outlives the call below. | ||
| unsafe { (self.func)(self.context as *mut c_void, bytes.as_ptr()) } | ||
| } | ||
| } | ||
|
|
||
| /// `on_built` as the optional observer reference the funding paths take. | ||
| pub(crate) fn observer_ref<F>(on_built: &Option<F>) -> Option<&AssetLockBuiltObserver<'_>> | ||
| where | ||
| F: Fn(OutPoint) + Send + Sync, | ||
| { | ||
| on_built | ||
| .as_ref() | ||
| .map(|notify| notify as &AssetLockBuiltObserver<'_>) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use dashcore::hashes::Hash; | ||
| use dashcore::Txid; | ||
| use std::sync::Mutex; | ||
|
|
||
| /// Appends the reported outpoint to the `Mutex<Vec<[u8; 36]>>` the | ||
| /// context points to. | ||
| unsafe extern "C" fn record(context: *mut c_void, out_point_36: *const u8) { | ||
| let received = &*(context as *const Mutex<Vec<[u8; 36]>>); | ||
| let mut bytes = [0u8; 36]; | ||
| std::ptr::copy_nonoverlapping(out_point_36, bytes.as_mut_ptr(), 36); | ||
| received.lock().unwrap().push(bytes); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_yield_no_callback_for_an_absent_function() { | ||
| assert!(AssetLockBuiltCallback::new(None, std::ptr::null_mut()).is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_pass_the_context_and_the_entry_layout_outpoint() { | ||
| let received = Mutex::new(Vec::new()); | ||
| let out_point = OutPoint::new(Txid::from_byte_array([7; 32]), 3); | ||
| let callback = AssetLockBuiltCallback::new( | ||
| Some(record), | ||
| &received as *const Mutex<Vec<[u8; 36]>> as *mut c_void, | ||
| ) | ||
| .expect("callback present"); | ||
|
|
||
| let on_built = Some(callback.into_observer()); | ||
| observer_ref(&on_built).expect("observer present")(out_point); | ||
|
|
||
| assert_eq!( | ||
| received.into_inner().unwrap(), | ||
| vec![outpoint_to_bytes(&out_point)] | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: Do not change existing C export signatures in place
The existing exported symbol
platform_address_wallet_fund_from_asset_lock_signernow requiresAssetLockBuiltFnand its context, and the same in-place ABI change is made toplatform_address_wallet_fund_from_asset_lock_external_signerand the two shielded funding exports. A host compiled against the previous C prototype will still call the unchanged symbol with the old argument count. The new callee then reads the callback and context from registers or stack locations that the old caller did not provide; if the callback value is non-null,AssetLockBuiltCallback::notifycan call an arbitrary address, causing undefined behavior or a process crash. Marking the PR as breaking and telling source callers to passNULL, NULLdoes not make an already-built binary compatible. Preserve the old symbols with their old signatures and add versioned/new symbols for the callback-enabled ABI, or introduce an explicit ABI version boundary and update rebuilt Swift/JNI callers to use it.source:
muse-spark-1.3-contributor(phase1-reviewer: general, rust-quality)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not changing this one: these exports have no separately built consumer, so the old-binary-on-new-library case does not occur.
librs_unified_sdk_ffi.afromDashSDKFFI.xcframework, whichbuild_ios.shbuilds from one tree together with the cbindgen header that Swift compiles against. The released xcframework zip ships both, and dashwallet-ios builds it at its.platform-ref. A mismatched prototype is a compile error there.rs-unified-sdk-jni, which calls them as Rust functions over the rlib (already updated in this PR). TheJava_*entry points Kotlin calls keep their signatures, andbuild_android.shdrops the standalonelibplatform_wallet_ffi.so.The repository does not keep these C prototypes stable across builds. The coding conventions treat even removing an
extern "C"function as ordinary work described in the PR body, and there is precedent on these very exports: #3793 addedsurplus_output_ptr/surplus_output_lentoplatform_wallet_manager_shielded_fund_from_asset_lockin place, and #5649 replaceddash_sdk_derive_connect_key_with_resolverwithout keeping the old symbol. Versioned duplicates here would add four exports that no matching build calls. Binary stability for the FFI would be a crate-wide policy, which is worth an issue of its own if maintainers want it. This change is listed under "C ABI" in the description.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bundled Swift static library and the JNI path are rebuilt together, so those in-tree consumers are not the counterexample. However,
platform-wallet-ffistill declaresstaticlib,cdylib, andrlib, and its README explicitly documents consuming the generated.so/.dylibfrom C or C++; those consumers can load a newer library with an older caller and are not protected by the synchronized build. The prior in-place changes do not establish ABI compatibility, and marking this release as breaking does not prevent the old symbol from being called with the old argument list, so the existing exports still need to retain their signatures or be supplemented with new symbols.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README's C/C++ section builds the library from source, and the same build runs cbindgen (
build.rs) to regenerateplatform_wallet_ffi.h, so a consumer that follows it gets the new prototype along with the new library. No release publisheslibplatform_wallet_ffi.so/.dylib:release-swift-sdk.ymlships the xcframework (static library plus header) andrelease-kotlin-sdk.ymlships the Android AAR, whoselibdash_sdk_jni.sois called only by the Kotlin classes packaged with it. Neither artifact lets an older caller resolve a newer library.The repository also does not treat these exports as a stable ABI: the coding conventions (
book/src/contributing/coding-conventions.md, "Commits, pull requests") class removing anextern "C"function as ordinaryfeat:/refactor:work described in the PR body. A changed signature is no stronger a break than a removed symbol. The error-code registry is a different case: those integers are mirrored by hand in Swift and Kotlin, so a renumbering compiles cleanly and is misread. A changed prototype fails to compile against the regenerated header.A versioned C ABI for
platform-wallet-ffi(old symbols kept as wrappers, suffixed callback-capable ones added) would be a policy for the whole crate and should be settled with the maintainers in its own issue, not introduced for four functions here. I'm leaving this thread open for them.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The synchronized Swift/JNI builds protect only callers rebuilt from this checkout; they do not protect a previously compiled C/C++ object or an application that links an older object against a newly built static or dynamic library. This crate explicitly produces
staticlibandcdylib, and its README documents C/C++ consumption, so regenerating the header makes a source rebuild fail or succeed correctly but does not make the existing symbol ABI-safe for already-compiled callers. Prior in-place breaks are precedent for the same compatibility problem, not evidence that this change is safe; these exports still need old-signature wrappers or distinct callback-enabled symbols.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Weighing in as an
rs-platform-wallet-ffiowner: I agree with Roman and don't consider this blocking.book/src/contributing/coding-conventions.mdclasses removing anextern "C"function as ordinaryfeat:/refactor:work, and feat(drive): shielded fees for Shield/ShieldFromAssetLock + shield credit conservation #3793 already appendedsurplus_output_ptr/surplus_output_lentoplatform_wallet_manager_shielded_fund_from_asset_lockin place.ERROR_CODE_REGISTRY.md's "shipped host binary" rule is about the integer codes, which Swift and Kotlin mirror by hand and which renumber silently. That is a different failure mode from a changed prototype.If maintainers want versioned C exports for this crate, that's a crate-wide policy and belongs in its own issue, not in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still consider this blocking. The repository itself documents the opposite compatibility assumption in
core_wallet_signed_payment_finalize: it explicitly preserves an old C signature because a host's compiled code andDashSDKFFI.xcframeworkcan meet at different versions, and warns that adding an argument in place can make the callee treat an absent argument as an address. That applies to these exports as well; the README documents bothstaticliband independently consumablecdyliboutputs, so rebuilding the generated header only protects source rebuilds, not an already-compiled C/C++ caller or separately packaged binary. The prior in-place change is precedent for the same ABI hazard, not evidence that this four-export change is safe; please retain wrappers with the old signatures or add distinct callback-enabled symbols.