Skip to content

Commit e38dcca

Browse files
committed
Allow holder commitment and HTLC signature requests to fail
As part of the ongoing async signer work, our holder signatures must also be capable of being obtained asynchronously. We expose a new `ChannelMonitor::signer_unblocked` method to retry pending onchain claims by re-signing and rebroadcasting transactions. Unfortunately, we cannot retry said claims without them being registered first, so if we're not able to obtain the signature synchronously, we must return the transaction as unsigned and ensure it is not broadcast.
1 parent 043ab75 commit e38dcca

11 files changed

+355
-65
lines changed

lightning/src/chain/chainmonitor.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,27 @@ where C::Target: chain::Filter,
635635
)
636636
}
637637
}
638+
639+
/// Triggers rebroadcasts of pending claims from force-closed channels after a transaction
640+
/// signature generation failure.
641+
///
642+
/// `monitor_opt` can be used as a filter to only trigger them for a specific channel monitor.
643+
pub fn signer_unblocked(&self, monitor_opt: Option<OutPoint>) {
644+
let monitors = self.monitors.read().unwrap();
645+
if let Some(funding_txo) = monitor_opt {
646+
if let Some(monitor_holder) = monitors.get(&funding_txo) {
647+
monitor_holder.monitor.signer_unblocked(
648+
&*self.broadcaster, &*self.fee_estimator, &self.logger
649+
)
650+
}
651+
} else {
652+
for (_, monitor_holder) in &*monitors {
653+
monitor_holder.monitor.signer_unblocked(
654+
&*self.broadcaster, &*self.fee_estimator, &self.logger
655+
)
656+
}
657+
}
658+
}
638659
}
639660

640661
impl<ChannelSigner: WriteableEcdsaChannelSigner, C: Deref, T: Deref, F: Deref, L: Deref, P: Deref>

lightning/src/chain/channelmonitor.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,6 +1769,25 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
17691769
);
17701770
}
17711771

1772+
/// Triggers rebroadcasts of pending claims from a force-closed channel after a transaction
1773+
/// signature generation failure.
1774+
pub fn signer_unblocked<B: Deref, F: Deref, L: Deref>(
1775+
&self, broadcaster: B, fee_estimator: F, logger: &L,
1776+
)
1777+
where
1778+
B::Target: BroadcasterInterface,
1779+
F::Target: FeeEstimator,
1780+
L::Target: Logger,
1781+
{
1782+
let fee_estimator = LowerBoundedFeeEstimator::new(fee_estimator);
1783+
let mut inner = self.inner.lock().unwrap();
1784+
let logger = WithChannelMonitor::from_impl(logger, &*inner);
1785+
let current_height = inner.best_block.height;
1786+
inner.onchain_tx_handler.rebroadcast_pending_claims(
1787+
current_height, FeerateStrategy::RetryPrevious, &broadcaster, &fee_estimator, &logger,
1788+
);
1789+
}
1790+
17721791
/// Returns the descriptors for relevant outputs (i.e., those that we can spend) within the
17731792
/// transaction if they exist and the transaction has at least [`ANTI_REORG_DELAY`]
17741793
/// confirmations. For [`SpendableOutputDescriptor::DelayedPaymentOutput`] descriptors to be
@@ -1811,6 +1830,12 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
18111830
pub fn set_counterparty_payment_script(&self, script: ScriptBuf) {
18121831
self.inner.lock().unwrap().counterparty_payment_script = script;
18131832
}
1833+
1834+
#[cfg(test)]
1835+
pub fn do_signer_call<F: FnMut(&Signer) -> ()>(&self, mut f: F) {
1836+
let inner = self.inner.lock().unwrap();
1837+
f(&inner.onchain_tx_handler.signer);
1838+
}
18141839
}
18151840

18161841
impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
@@ -3526,9 +3551,12 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
35263551
continue;
35273552
}
35283553
} else { None };
3529-
if let Some(htlc_tx) = self.onchain_tx_handler.get_fully_signed_htlc_tx(
3530-
&::bitcoin::OutPoint { txid, vout }, &preimage) {
3531-
holder_transactions.push(htlc_tx);
3554+
if let Some(htlc_tx) = self.onchain_tx_handler.get_maybe_signed_htlc_tx(
3555+
&::bitcoin::OutPoint { txid, vout }, &preimage
3556+
) {
3557+
if htlc_tx.is_fully_signed() {
3558+
holder_transactions.push(htlc_tx.0);
3559+
}
35323560
}
35333561
}
35343562
}

lightning/src/chain/onchaintx.rs

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use crate::chain::ClaimId;
3131
use crate::chain::chaininterface::{ConfirmationTarget, FeeEstimator, BroadcasterInterface, LowerBoundedFeeEstimator};
3232
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, CLTV_SHARED_CLAIM_BUFFER};
3333
use crate::chain::package::{PackageSolvingData, PackageTemplate};
34+
use crate::chain::transaction::MaybeSignedTransaction;
3435
use crate::util::logger::Logger;
3536
use crate::util::ser::{Readable, ReadableArgs, MaybeReadable, UpgradableRequired, Writer, Writeable, VecWriter};
3637

@@ -204,14 +205,16 @@ pub(crate) enum ClaimEvent {
204205
/// control) onchain.
205206
pub(crate) enum OnchainClaim {
206207
/// A finalized transaction pending confirmation spending the output to claim.
207-
Tx(Transaction),
208+
Tx(MaybeSignedTransaction),
208209
/// An event yielded externally to signal additional inputs must be added to a transaction
209210
/// pending confirmation spending the output to claim.
210211
Event(ClaimEvent),
211212
}
212213

213-
/// Represents the different feerates a pending request can use when generating a claim.
214+
/// Represents the different feerate strategies a pending request can use when generating a claim.
214215
pub(crate) enum FeerateStrategy {
216+
/// We must reuse the most recently used feerate, if any.
217+
RetryPrevious,
215218
/// We must pick the highest between the most recently used and the current feerate estimate.
216219
HighestOfPreviousOrNew,
217220
/// We must force a bump of the most recently used feerate, either by using the current feerate
@@ -506,9 +509,13 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
506509
}
507510
match claim {
508511
OnchainClaim::Tx(tx) => {
509-
let log_start = if bumped_feerate { "Broadcasting RBF-bumped" } else { "Rebroadcasting" };
510-
log_info!(logger, "{} onchain {}", log_start, log_tx!(tx));
511-
broadcaster.broadcast_transactions(&[&tx]);
512+
if tx.is_fully_signed() {
513+
let log_start = if bumped_feerate { "Broadcasting RBF-bumped" } else { "Rebroadcasting" };
514+
log_info!(logger, "{} onchain {}", log_start, log_tx!(tx.0));
515+
broadcaster.broadcast_transactions(&[&tx.0]);
516+
} else {
517+
log_info!(logger, "Waiting for signature of unsigned onchain transaction {}", tx.0.txid());
518+
}
512519
},
513520
OnchainClaim::Event(event) => {
514521
let log_start = if bumped_feerate { "Yielding fee-bumped" } else { "Replaying" };
@@ -610,11 +617,10 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
610617
) {
611618
assert!(new_feerate != 0);
612619

613-
let transaction = cached_request.finalize_malleable_package(
620+
let transaction = cached_request.maybe_finalize_malleable_package(
614621
cur_height, self, output_value, self.destination_script.clone(), logger
615622
).unwrap();
616-
log_trace!(logger, "...with timer {} and feerate {}", new_timer, new_feerate);
617-
assert!(predicted_weight >= transaction.weight().to_wu());
623+
assert!(predicted_weight >= transaction.0.weight().to_wu());
618624
return Some((new_timer, new_feerate, OnchainClaim::Tx(transaction)));
619625
}
620626
} else {
@@ -623,7 +629,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
623629
// which require external funding.
624630
let mut inputs = cached_request.inputs();
625631
debug_assert_eq!(inputs.len(), 1);
626-
let tx = match cached_request.finalize_untractable_package(self, logger) {
632+
let tx = match cached_request.maybe_finalize_untractable_package(self, logger) {
627633
Some(tx) => tx,
628634
None => return None,
629635
};
@@ -634,27 +640,27 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
634640
// Commitment inputs with anchors support are the only untractable inputs supported
635641
// thus far that require external funding.
636642
PackageSolvingData::HolderFundingOutput(output) => {
637-
debug_assert_eq!(tx.txid(), self.holder_commitment.trust().txid(),
643+
debug_assert_eq!(tx.0.txid(), self.holder_commitment.trust().txid(),
638644
"Holder commitment transaction mismatch");
639645

640646
let conf_target = ConfirmationTarget::OnChainSweep;
641647
let package_target_feerate_sat_per_1000_weight = cached_request
642648
.compute_package_feerate(fee_estimator, conf_target, feerate_strategy);
643649
if let Some(input_amount_sat) = output.funding_amount {
644-
let fee_sat = input_amount_sat - tx.output.iter().map(|output| output.value).sum::<u64>();
650+
let fee_sat = input_amount_sat - tx.0.output.iter().map(|output| output.value).sum::<u64>();
645651
let commitment_tx_feerate_sat_per_1000_weight =
646-
compute_feerate_sat_per_1000_weight(fee_sat, tx.weight().to_wu());
652+
compute_feerate_sat_per_1000_weight(fee_sat, tx.0.weight().to_wu());
647653
if commitment_tx_feerate_sat_per_1000_weight >= package_target_feerate_sat_per_1000_weight {
648-
log_debug!(logger, "Pre-signed {} already has feerate {} sat/kW above required {} sat/kW",
649-
log_tx!(tx), commitment_tx_feerate_sat_per_1000_weight,
654+
log_debug!(logger, "Pre-signed commitment {} already has feerate {} sat/kW above required {} sat/kW",
655+
tx.0.txid(), commitment_tx_feerate_sat_per_1000_weight,
650656
package_target_feerate_sat_per_1000_weight);
651657
return Some((new_timer, 0, OnchainClaim::Tx(tx.clone())));
652658
}
653659
}
654660

655661
// We'll locate an anchor output we can spend within the commitment transaction.
656662
let funding_pubkey = &self.channel_transaction_parameters.holder_pubkeys.funding_pubkey;
657-
match chan_utils::get_anchor_output(&tx, funding_pubkey) {
663+
match chan_utils::get_anchor_output(&tx.0, funding_pubkey) {
658664
// An anchor output was found, so we should yield a funding event externally.
659665
Some((idx, _)) => {
660666
// TODO: Use a lower confirmation target when both our and the
@@ -664,7 +670,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
664670
package_target_feerate_sat_per_1000_weight as u64,
665671
OnchainClaim::Event(ClaimEvent::BumpCommitment {
666672
package_target_feerate_sat_per_1000_weight,
667-
commitment_tx: tx.clone(),
673+
commitment_tx: tx.0.clone(),
668674
anchor_output_idx: idx,
669675
}),
670676
))
@@ -785,9 +791,13 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
785791
// `OnchainClaim`.
786792
let claim_id = match claim {
787793
OnchainClaim::Tx(tx) => {
788-
log_info!(logger, "Broadcasting onchain {}", log_tx!(tx));
789-
broadcaster.broadcast_transactions(&[&tx]);
790-
ClaimId(tx.txid().to_byte_array())
794+
if tx.is_fully_signed() {
795+
log_info!(logger, "Broadcasting onchain {}", log_tx!(tx.0));
796+
broadcaster.broadcast_transactions(&[&tx.0]);
797+
} else {
798+
log_info!(logger, "Waiting for signature of unsigned onchain transaction {}", tx.0.txid());
799+
}
800+
ClaimId(tx.0.txid().to_byte_array())
791801
},
792802
OnchainClaim::Event(claim_event) => {
793803
log_info!(logger, "Yielding onchain event to spend inputs {:?}", req.outpoints());
@@ -980,8 +990,13 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
980990
) {
981991
match bump_claim {
982992
OnchainClaim::Tx(bump_tx) => {
983-
log_info!(logger, "Broadcasting RBF-bumped onchain {}", log_tx!(bump_tx));
984-
broadcaster.broadcast_transactions(&[&bump_tx]);
993+
if bump_tx.is_fully_signed() {
994+
log_info!(logger, "Broadcasting RBF-bumped onchain {}", log_tx!(bump_tx.0));
995+
broadcaster.broadcast_transactions(&[&bump_tx.0]);
996+
} else {
997+
log_info!(logger, "Waiting for signature of RBF-bumped unsigned onchain transaction {}",
998+
bump_tx.0.txid());
999+
}
9851000
},
9861001
OnchainClaim::Event(claim_event) => {
9871002
log_info!(logger, "Yielding RBF-bumped onchain event to spend inputs {:?}", request.outpoints());
@@ -1063,8 +1078,12 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
10631078
request.set_feerate(new_feerate);
10641079
match bump_claim {
10651080
OnchainClaim::Tx(bump_tx) => {
1066-
log_info!(logger, "Broadcasting onchain {}", log_tx!(bump_tx));
1067-
broadcaster.broadcast_transactions(&[&bump_tx]);
1081+
if bump_tx.is_fully_signed() {
1082+
log_info!(logger, "Broadcasting onchain {}", log_tx!(bump_tx.0));
1083+
broadcaster.broadcast_transactions(&[&bump_tx.0]);
1084+
} else {
1085+
log_info!(logger, "Waiting for signature of unsigned onchain transaction {}", bump_tx.0.txid());
1086+
}
10681087
},
10691088
OnchainClaim::Event(claim_event) => {
10701089
log_info!(logger, "Yielding onchain event after reorg to spend inputs {:?}", request.outpoints());
@@ -1117,13 +1136,11 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
11171136
&self.holder_commitment.trust().built_transaction().transaction
11181137
}
11191138

1120-
//TODO: getting lastest holder transactions should be infallible and result in us "force-closing the channel", but we may
1121-
// have empty holder commitment transaction if a ChannelMonitor is asked to force-close just after OutboundV1Channel::get_funding_created,
1122-
// before providing a initial commitment transaction. For outbound channel, init ChannelMonitor at Channel::funding_signed, there is nothing
1123-
// to monitor before.
1124-
pub(crate) fn get_fully_signed_holder_tx(&mut self, funding_redeemscript: &Script) -> Transaction {
1125-
let sig = self.signer.sign_holder_commitment(&self.holder_commitment, &self.secp_ctx).expect("signing holder commitment");
1126-
self.holder_commitment.add_holder_sig(funding_redeemscript, sig)
1139+
pub(crate) fn get_maybe_signed_holder_tx(&mut self, funding_redeemscript: &Script) -> MaybeSignedTransaction {
1140+
let tx = self.signer.sign_holder_commitment(&self.holder_commitment, &self.secp_ctx)
1141+
.map(|sig| self.holder_commitment.add_holder_sig(funding_redeemscript, sig))
1142+
.unwrap_or_else(|_| self.get_unsigned_holder_commitment_tx().clone());
1143+
MaybeSignedTransaction(tx)
11271144
}
11281145

11291146
#[cfg(any(test, feature="unsafe_revoked_tx_signing"))]
@@ -1132,7 +1149,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
11321149
self.holder_commitment.add_holder_sig(funding_redeemscript, sig)
11331150
}
11341151

1135-
pub(crate) fn get_fully_signed_htlc_tx(&mut self, outp: &::bitcoin::OutPoint, preimage: &Option<PaymentPreimage>) -> Option<Transaction> {
1152+
pub(crate) fn get_maybe_signed_htlc_tx(&mut self, outp: &::bitcoin::OutPoint, preimage: &Option<PaymentPreimage>) -> Option<MaybeSignedTransaction> {
11361153
let get_signed_htlc_tx = |holder_commitment: &HolderCommitmentTransaction| {
11371154
let trusted_tx = holder_commitment.trust();
11381155
if trusted_tx.txid() != outp.txid {
@@ -1160,11 +1177,12 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
11601177
preimage: preimage.clone(),
11611178
counterparty_sig: counterparty_htlc_sig.clone(),
11621179
};
1163-
let htlc_sig = self.signer.sign_holder_htlc_transaction(&htlc_tx, 0, &htlc_descriptor, &self.secp_ctx).unwrap();
1164-
htlc_tx.input[0].witness = trusted_tx.build_htlc_input_witness(
1165-
htlc_idx, &counterparty_htlc_sig, &htlc_sig, preimage,
1166-
);
1167-
Some(htlc_tx)
1180+
if let Ok(htlc_sig) = self.signer.sign_holder_htlc_transaction(&htlc_tx, 0, &htlc_descriptor, &self.secp_ctx) {
1181+
htlc_tx.input[0].witness = trusted_tx.build_htlc_input_witness(
1182+
htlc_idx, &counterparty_htlc_sig, &htlc_sig, preimage,
1183+
);
1184+
}
1185+
Some(MaybeSignedTransaction(htlc_tx))
11681186
};
11691187

11701188
// Check if the HTLC spends from the current holder commitment first, or the previous.

lightning/src/chain/package.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::ln::features::ChannelTypeFeatures;
2828
use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint};
2929
use crate::ln::msgs::DecodeError;
3030
use crate::chain::chaininterface::{FeeEstimator, ConfirmationTarget, MIN_RELAY_FEE_SAT_PER_1000_WEIGHT, compute_feerate_sat_per_1000_weight, FEERATE_FLOOR_SATS_PER_KW};
31+
use crate::chain::transaction::MaybeSignedTransaction;
3132
use crate::sign::ecdsa::WriteableEcdsaChannelSigner;
3233
use crate::chain::onchaintx::{FeerateStrategy, ExternalHTLCClaim, OnchainTxHandler};
3334
use crate::util::logger::Logger;
@@ -633,14 +634,14 @@ impl PackageSolvingData {
633634
}
634635
true
635636
}
636-
fn get_finalized_tx<Signer: WriteableEcdsaChannelSigner>(&self, outpoint: &BitcoinOutPoint, onchain_handler: &mut OnchainTxHandler<Signer>) -> Option<Transaction> {
637+
fn get_maybe_finalized_tx<Signer: WriteableEcdsaChannelSigner>(&self, outpoint: &BitcoinOutPoint, onchain_handler: &mut OnchainTxHandler<Signer>) -> Option<MaybeSignedTransaction> {
637638
match self {
638639
PackageSolvingData::HolderHTLCOutput(ref outp) => {
639640
debug_assert!(!outp.channel_type_features.supports_anchors_zero_fee_htlc_tx());
640-
return onchain_handler.get_fully_signed_htlc_tx(outpoint, &outp.preimage);
641+
onchain_handler.get_maybe_signed_htlc_tx(outpoint, &outp.preimage)
641642
}
642643
PackageSolvingData::HolderFundingOutput(ref outp) => {
643-
return Some(onchain_handler.get_fully_signed_holder_tx(&outp.funding_redeemscript));
644+
Some(onchain_handler.get_maybe_signed_holder_tx(&outp.funding_redeemscript))
644645
}
645646
_ => { panic!("API Error!"); }
646647
}
@@ -908,10 +909,10 @@ impl PackageTemplate {
908909
}
909910
htlcs
910911
}
911-
pub(crate) fn finalize_malleable_package<L: Logger, Signer: WriteableEcdsaChannelSigner>(
912+
pub(crate) fn maybe_finalize_malleable_package<L: Logger, Signer: WriteableEcdsaChannelSigner>(
912913
&self, current_height: u32, onchain_handler: &mut OnchainTxHandler<Signer>, value: u64,
913914
destination_script: ScriptBuf, logger: &L
914-
) -> Option<Transaction> {
915+
) -> Option<MaybeSignedTransaction> {
915916
debug_assert!(self.is_malleable());
916917
let mut bumped_tx = Transaction {
917918
version: 2,
@@ -927,19 +928,17 @@ impl PackageTemplate {
927928
}
928929
for (i, (outpoint, out)) in self.inputs.iter().enumerate() {
929930
log_debug!(logger, "Adding claiming input for outpoint {}:{}", outpoint.txid, outpoint.vout);
930-
if !out.finalize_input(&mut bumped_tx, i, onchain_handler) { return None; }
931+
if !out.finalize_input(&mut bumped_tx, i, onchain_handler) { continue; }
931932
}
932-
log_debug!(logger, "Finalized transaction {} ready to broadcast", bumped_tx.txid());
933-
Some(bumped_tx)
933+
Some(MaybeSignedTransaction(bumped_tx))
934934
}
935-
pub(crate) fn finalize_untractable_package<L: Logger, Signer: WriteableEcdsaChannelSigner>(
935+
pub(crate) fn maybe_finalize_untractable_package<L: Logger, Signer: WriteableEcdsaChannelSigner>(
936936
&self, onchain_handler: &mut OnchainTxHandler<Signer>, logger: &L,
937-
) -> Option<Transaction> {
937+
) -> Option<MaybeSignedTransaction> {
938938
debug_assert!(!self.is_malleable());
939939
if let Some((outpoint, outp)) = self.inputs.first() {
940-
if let Some(final_tx) = outp.get_finalized_tx(outpoint, onchain_handler) {
940+
if let Some(final_tx) = outp.get_maybe_finalized_tx(outpoint, onchain_handler) {
941941
log_debug!(logger, "Adding claiming input for outpoint {}:{}", outpoint.txid, outpoint.vout);
942-
log_debug!(logger, "Finalized transaction {} ready to broadcast", final_tx.txid());
943942
return Some(final_tx);
944943
}
945944
return None;
@@ -996,6 +995,7 @@ impl PackageTemplate {
996995
if self.feerate_previous != 0 {
997996
let previous_feerate = self.feerate_previous.try_into().unwrap_or(u32::max_value());
998997
match feerate_strategy {
998+
FeerateStrategy::RetryPrevious => previous_feerate,
999999
FeerateStrategy::HighestOfPreviousOrNew => cmp::max(previous_feerate, feerate_estimate),
10001000
FeerateStrategy::ForceBump => if feerate_estimate > previous_feerate {
10011001
feerate_estimate
@@ -1141,6 +1141,10 @@ where
11411141
// If old feerate inferior to actual one given back by Fee Estimator, use it to compute new fee...
11421142
let (new_fee, new_feerate) = if let Some((new_fee, new_feerate)) = compute_fee_from_spent_amounts(input_amounts, predicted_weight, fee_estimator, logger) {
11431143
match feerate_strategy {
1144+
FeerateStrategy::RetryPrevious => {
1145+
let previous_fee = previous_feerate * predicted_weight / 1000;
1146+
(previous_fee, previous_feerate)
1147+
},
11441148
FeerateStrategy::HighestOfPreviousOrNew => if new_feerate > previous_feerate {
11451149
(new_fee, new_feerate)
11461150
} else {

lightning/src/chain/transaction.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub struct OutPoint {
5858
impl OutPoint {
5959
/// Converts this OutPoint into the OutPoint field as used by rust-bitcoin
6060
///
61-
/// This is not exported to bindings users as the same type is used universally in the C bindings
61+
/// This is not exported to bindings users as the same type is used universally in the C bindings
6262
/// for all outpoints
6363
pub fn into_bitcoin_outpoint(self) -> BitcoinOutPoint {
6464
BitcoinOutPoint {
@@ -76,6 +76,15 @@ impl core::fmt::Display for OutPoint {
7676

7777
impl_writeable!(OutPoint, { txid, index });
7878

79+
#[derive(Debug, Clone)]
80+
pub(crate) struct MaybeSignedTransaction(pub Transaction);
81+
82+
impl MaybeSignedTransaction {
83+
pub fn is_fully_signed(&self) -> bool {
84+
!self.0.input.iter().any(|input| input.witness.is_empty())
85+
}
86+
}
87+
7988
#[cfg(test)]
8089
mod tests {
8190
use crate::chain::transaction::OutPoint;

0 commit comments

Comments
 (0)