Problem
transaction::multisign has a signature that conflates two lifetimes that
should be independent:
pub fn multisign<'a, T, F>(transaction: &mut T, tx_list: &'a Vec<T>) -> XRPLHelperResult<()>
where
F: IntoEnumIterator + Serialize + Debug + PartialEq + 'a,
T: Transaction<'a, F>,
'a is used both as:
- The borrow lifetime of
tx_list.
- The data lifetime of
T: Transaction<'a, F>.
Because they share one parameter, the compiler must pick a single 'a that
satisfies both. Once a caller's T lifetime is inferred as 'static — which
happens routinely when the transaction is built from string literals
(XRPAmount::from("20000000") → Cow::Borrowed<'static, str>) and used in
an async block that pushes the inferred lifetime to 'static — the borrow
of tx_list is also required to be 'static.
Observed symptom
Callers cannot pass a &local_vec to multisign from an async test that
also performs autofill: they have to Box::leak the Vec to obtain a
&'static Vec<T>. Concrete instance introduced by PR #298 (commit 6031ad2):
tests/transactions/multisign_payment.rs lines 81–90:
https://github.com/XRPLF/xrpl-rust/blob/6031ad2/tests/transactions/multisign_payment.rs#L81-L90
// multisign() merges the signer-signed copies into the master transaction.
// Box::leak the Vec because multisign's signature ties the borrow's lifetime
// to T's 'a parameter, which the async block infers as 'static when the
// Payment was built from string literals (e.g. XRPAmount::from("20000000")).
// Without 'static here the borrow checker rejects the call. The leak is
// intentional and bounded by the test runtime.
let signer_signed_copies: &'static Vec<_> =
Box::leak(Box::new(vec![payment_signed_by_a, payment_signed_by_b]));
xrpl::transaction::multisign(&mut payment, signer_signed_copies)
.expect("multisign combine");
Every external caller (not just tests) will hit this. Box::leak is not an
acceptable steady state for library users.
Proposed fixes
Option A — decouple borrow lifetime, accept a slice (minimal change)
pub fn multisign<'a, 'b, T, F>(transaction: &mut T, tx_list: &'b [T]) -> XRPLHelperResult<()>
where
F: IntoEnumIterator + Serialize + Debug + PartialEq + 'a,
T: Transaction<'a, F>,
'b is the short borrow lifetime; 'a remains Transaction's data lifetime.
The function body never stores tx_list past the call — it iterates once
and clones the first signer of each — so a short borrow is sufficient.
Bonus: switching &Vec<T> → &[T] is more idiomatic, and existing callers
passing &vec continue to work via Vec → slice coercion.
Option B — take an iterator, consuming the txs (more flexible)
pub fn multisign<'a, T, F, I>(transaction: &mut T, tx_list: I) -> XRPLHelperResult<()>
where
I: IntoIterator<Item = T>,
T: Transaction<'a, F> + Clone,
F: IntoEnumIterator + Serialize + Debug + PartialEq + 'a,
Function consumes each signer-tx; no borrow at all. Callers pass
vec![a, b] directly. Trade-off: the call site now moves the Vec rather
than borrowing it, so callers can't reuse it afterward (rarely needed
here — the signer copies exist only to be merged).
Slightly larger refactor: tx_list: I is consumed, and the loop body
already does decoded_tx_signers.push(tx_signer.clone()) on the first
signer of each tx, so adapting from for tx in tx_list (which currently
borrows each element since tx_list: &Vec<T>) to for tx in tx_list
(now owning each element via IntoIterator) requires either
tx.get_common_fields() to remain valid on an owned T (it does), or
threading borrows differently. Either way, no borrow lifetime propagates
back to the caller.
Recommendation
Option A is the smallest backward-compatible change; option B is
structurally cleaner. Both eliminate the Box::leak requirement. I'd ship
A as a zero-risk fix and consider B alongside any future refactor of the
multisign ergonomics.
Acceptance
Problem
transaction::multisignhas a signature that conflates two lifetimes thatshould be independent:
'ais used both as:tx_list.T: Transaction<'a, F>.Because they share one parameter, the compiler must pick a single
'athatsatisfies both. Once a caller's
Tlifetime is inferred as'static— whichhappens routinely when the transaction is built from string literals
(
XRPAmount::from("20000000")→Cow::Borrowed<'static, str>) and used inan async block that pushes the inferred lifetime to
'static— the borrowof
tx_listis also required to be'static.Observed symptom
Callers cannot pass a
&local_vectomultisignfrom an async test thatalso performs autofill: they have to
Box::leakthe Vec to obtain a&'static Vec<T>. Concrete instance introduced by PR #298 (commit6031ad2):tests/transactions/multisign_payment.rslines 81–90:https://github.com/XRPLF/xrpl-rust/blob/6031ad2/tests/transactions/multisign_payment.rs#L81-L90
Every external caller (not just tests) will hit this.
Box::leakis not anacceptable steady state for library users.
Proposed fixes
Option A — decouple borrow lifetime, accept a slice (minimal change)
'bis the short borrow lifetime;'aremains Transaction's data lifetime.The function body never stores
tx_listpast the call — it iterates onceand clones the first signer of each — so a short borrow is sufficient.
Bonus: switching
&Vec<T>→&[T]is more idiomatic, and existing callerspassing
&veccontinue to work via Vec → slice coercion.Option B — take an iterator, consuming the txs (more flexible)
Function consumes each signer-tx; no borrow at all. Callers pass
vec![a, b]directly. Trade-off: the call site now moves the Vec ratherthan borrowing it, so callers can't reuse it afterward (rarely needed
here — the signer copies exist only to be merged).
Slightly larger refactor:
tx_list: Iis consumed, and the loop bodyalready does
decoded_tx_signers.push(tx_signer.clone())on the firstsigner of each tx, so adapting from
for tx in tx_list(which currentlyborrows each element since
tx_list: &Vec<T>) tofor tx in tx_list(now owning each element via
IntoIterator) requires eithertx.get_common_fields()to remain valid on an ownedT(it does), orthreading borrows differently. Either way, no borrow lifetime propagates
back to the caller.
Recommendation
Option A is the smallest backward-compatible change; option B is
structurally cleaner. Both eliminate the
Box::leakrequirement. I'd shipA as a zero-risk fix and consider B alongside any future refactor of the
multisign ergonomics.
Acceptance
src/transaction/multisign.rsto remove theborrow-lifetime / Transaction-lifetime coupling (option A or B).
tests/transactions/multisign_payment.rs(PR feat: enforce 65% integration-test coverage and add tests #298, lines81–90) to drop the
Box::leak.src/transaction/multisign.rs::testsstill compiles.