Skip to content

Add Shared Input support in interactive TX construction #3842

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

optout21
Copy link
Contributor

@optout21 optout21 commented Jun 10, 2025

In interactive TX construction, add support for shared input:

  • if we expect a shared input, make sure that the remote node provides it
  • if we provide a shared input, make sure that it's accounted appropriately

Additionally, the prevtx field of the TxAddInput message is changed to Optional, as it should not be set for the shared input (it cannot, as the full funding transaction is not stored on the acceptor side) (spec discussion: lightning/bolts#1160 (comment))

To be used by splicing, see #3736 .

Note: this PR does not include splicing negotiation.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 10, 2025

👋 Thanks for assigning @dunxen as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@optout21 optout21 marked this pull request as ready for review June 11, 2025 19:03
@optout21 optout21 requested review from jkczyz, wpaulino and dunxen June 11, 2025 19:03
prevtx_out,
sequence,
}, {
(0, shared_input_txid, option), // `funding_txid`
(0, prevtx, option),
(1, shared_input_txid, option), // `funding_txid`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually TLV 0, and there's no TLV for prevtx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we'll need to unroll the serialization macro. We need to read prevtx_len and only if it's 0, do we need to read the shared_input_txid TLV.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need help with that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this:

diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs
index 76db8e67a..6f43ca8c1 100644
--- a/lightning/src/ln/msgs.rs
+++ b/lightning/src/ln/msgs.rs
@@ -29,7 +29,7 @@ use bitcoin::hash_types::Txid;
 use bitcoin::script::ScriptBuf;
 use bitcoin::secp256k1::ecdsa::Signature;
 use bitcoin::secp256k1::PublicKey;
-use bitcoin::{secp256k1, Witness};
+use bitcoin::{secp256k1, Transaction, Witness};
 
 use crate::blinded_path::payment::{
 	BlindedPaymentTlvs, ForwardTlvs, ReceiveTlvs, UnauthenticatedReceiveTlvs,
@@ -2668,15 +2668,59 @@ impl_writeable_msg!(SpliceLocked, {
 	splice_txid,
 }, {});
 
-impl_writeable_msg!(TxAddInput, {
-	channel_id,
-	serial_id,
-	prevtx_out,
-	sequence,
-}, {
-	(0, prevtx, option),
-	(1, shared_input_txid, option), // `funding_txid`
-});
+impl Writeable for TxAddInput {
+	fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> {
+		self.channel_id.write(w)?;
+		self.serial_id.write(w)?;
+		if let Some(prevtx) = self.prevtx.as_ref() {
+			debug_assert!(self.shared_input_txid.is_none());
+			prevtx.write(w)?;
+		} else {
+			debug_assert!(self.shared_input_txid.is_some());
+			0u16.write(w)?;
+		}
+		self.prevtx_out.write(w)?;
+		self.sequence.write(w)?;
+
+		if let Some(shared_input_txid) = self.shared_input_txid.as_ref() {
+			encode_tlv_stream!(w, { (0, shared_input_txid, required) });
+		} else {
+			encode_tlv_stream!(w, {});
+		}
+
+		Ok(())
+	}
+}
+impl LengthReadable for TxAddInput {
+	fn read_from_fixed_length_buffer<R: LengthLimitedRead>(r: &mut R) -> Result<Self, DecodeError> {
+		let channel_id = Readable::read(r)?;
+		let serial_id = Readable::read(r)?;
+		let prevtx_len = <u16 as Readable>::read(r)?;
+		let mut prevtx = None;
+		if prevtx_len > 0 {
+			let mut tx_reader = FixedLengthReader::new(r, prevtx_len as u64);
+			let tx: Transaction = Readable::read(&mut tx_reader)?;
+			if tx_reader.bytes_remain() {
+				return Err(DecodeError::BadLengthDescriptor);
+			}
+			prevtx =
+				Some(TransactionU16LenLimited::new(tx).map_err(|_| DecodeError::InvalidValue)?);
+		}
+		let prevtx_out = Readable::read(r)?;
+		let sequence = Readable::read(r)?;
+
+		let mut shared_input_txid = None;
+		if prevtx_len > 0 {
+			decode_tlv_stream!(r, {});
+		} else {
+			decode_tlv_stream!(r, {
+				(0, shared_input_txid, required),
+			});
+		}
+
+		Ok(Self { channel_id, serial_id, prevtx, prevtx_out, sequence, shared_input_txid })
+	}
+}
 
 impl_writeable_msg!(TxAddOutput, {
 	channel_id,

/// malleable.
pub prevtx: TransactionU16LenLimited,
/// malleable. Omitted for shared input.
pub prevtx: Option<TransactionU16LenLimited>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make this an enum along with shared_input_txid, but I'm not sure what's a good name to call it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not a good suggestion, but how about just TxInputOrigin with Txid and PrevTx variants?

@@ -551,6 +570,27 @@ impl NegotiationContext {
Ok(shared_output)
}

fn set_actual_funding_input(&mut self, txin: TxIn) -> Result<SharedOwnedInput, AbortReason> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to do any of this, can't we rely on prevtx_outpoints to know if it was already added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how can we know whether an OutPoint in prevtx_outpoints is shared or not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to care if it is shared or not, just that it hasn't been added twice

@@ -890,6 +990,10 @@ impl NegotiationContext {
return Err(AbortReason::MissingFundingOutput);
}

if self.expected_shared_funding_input.is_some() && self.actual_funding_input.is_none() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, use prevtx_outpoints to see if the input was added

} = args;

// Sanity check: There can be at most one shared input, local-added or remote-added
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced we need this NegotiationInput abstraction, it seems to complicate things a good bit. Can we just keep inputs_to_contribute as we had it, and use expected_shared_funding_input along with is_initiator to know if we have to add it in maybe_send_message? Seems like the same could be done for expected_shared_funding_output, I'm not sure why the OutputOwned abstraction is needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I guess we do need it, but I managed to clean up how the shared funding output works and think we could do something similar for the shared funding input.

Here's a diff of the changes I made that can be applied to main (it doesn't fix tests): https://gist.github.com/wpaulino/d8e6282ecd63b250a6cc5820b25c7ca7

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this could be simplified, after some considerations your diff looks right, but I need to try it with the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've hacked the tests to work with your change, and they are generally OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your change, the semantics for the shared_funding_output is:

  • if we are the initiator, we want this funding output, and we will add it as an output in the negotiation
  • if we are the acceptor, we want this funding output, and we expect the initiator to add it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few cases which will not be possible to express with this change:

  • adding multiple shared outputs
  • acceptor adding a shared output.

As these are invalid combinations, it is fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, can you push those changes here then and rework the shared funding input work to follow a similar approach?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants