Skip to content

smite: add commitment_signed message codec implementation#24

Open
kartikangiras wants to merge 1 commit intomorehouse:masterfrom
kartikangiras:codec-commitment-signed
Open

smite: add commitment_signed message codec implementation#24
kartikangiras wants to merge 1 commit intomorehouse:masterfrom
kartikangiras:codec-commitment-signed

Conversation

@kartikangiras
Copy link
Copy Markdown

Add support for commitment-signed codec for BOLT 2 message.

@kartikangiras
Copy link
Copy Markdown
Author

Hi @morehouse , Could you PTAL into this PR, it is the part of milestone 2 adding support for commitment-signed codec (channel-ready is implemented in other PR by another contributor). Thanks.

Copy link
Copy Markdown
Owner

@morehouse morehouse left a comment

Choose a reason for hiding this comment

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

Thanks for working on this. Please make sure CI passes before requesting another review. You should be able to run all the CI commands locally (e.g., cargo test, cargo fmt, cargo clippy --all-targets --all-features -- -D warnings)

/// `funding_signed` message (BOLT 2).
pub const FUNDING_SIGNED: u16 = 35;
/// `commitment_signed` message (BOLT 2).
pub const COMMITMENT_SIGNED: u16 = 132;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

In this file we have been keeping the messages sorted by type number. Please reorder accordingly.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

You still haven't addressed the message order here or below.

let mut out = Vec::new();
self.channel_id.write(&mut out);
self.signature.write(&mut out);
(self.htlc_signatures.len() as u16).write(&mut out);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

We should panic if this cast would overflow, similar to the other codecs. I'd suggesting adding an assert to the beginning of encode:

assert!(self.htlc_signatures.len() <= u16::MAX as usize, "too many HTLC signatures");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@morehouse I was wondering if making WireFormat for Vec<u8> generic would be useful. This passes all existing tests:

patch
diff --git a/smite/src/bolt/wire.rs b/smite/src/bolt/wire.rs
index ebfef88..513389e 100644
--- a/smite/src/bolt/wire.rs
+++ b/smite/src/bolt/wire.rs
@@ -146,7 +146,7 @@ impl WireFormat for BigSize {
     }
 }

-impl WireFormat for Vec<u8> {
+impl<T: WireFormat> WireFormat for Vec<T> {
     /// Reads a `[u16:len][len*byte]` variable-length field, advancing past both.
     ///
     /// # Errors
@@ -160,16 +160,20 @@ impl WireFormat for Vec<u8> {
                 actual: data.len(),
             });
         }
-        let bytes = data[..len].to_vec();
-        *data = &data[len..];
-        Ok(bytes)
+        let mut vec = Vec::with_capacity(len);
+        for _ in 0..len {
+            vec.push(T::read(data)?);
+        }
+        Ok(vec)
     }

     /// Writes a `[u16:len][len*byte]` variable-length field.
     #[allow(clippy::cast_possible_truncation)]
     fn write(&self, out: &mut Vec<u8>) {
         (self.len() as u16).write(out);
-        out.extend_from_slice(self);
+        for item in self {
+            item.write(out);
+        }
     }
 }

However, I searched for num_ in bolt02 and only found witnesses in tx_signatures as another case where this could be useful.

So I think the generic interface would be overkill. Writing tests for it also feels arbitrary because of the decision for which types to write tests. I wrote some for Vec<Signature>:

tests
diff --git a/smite/src/bolt/wire.rs b/smite/src/bolt/wire.rs
index 513389e..a3dabf9 100644
--- a/smite/src/bolt/wire.rs
+++ b/smite/src/bolt/wire.rs
@@ -542,6 +542,34 @@ mod tests {
         assert!(cursor.is_empty());
     }

+    fn sample_signatures() -> Vec<Signature> {
+        let secp = Secp256k1::new();
+        let sk = SecretKey::from_byte_array([0x12; 32]).unwrap();
+        vec![
+            secp.sign_ecdsa(secp256k1::Message::from_digest([0x00; 32]), &sk),
+            secp.sign_ecdsa(secp256k1::Message::from_digest([0xAA; 32]), &sk),
+        ]
+    }
+
+    #[test]
+    fn vec_signature_read_empty_field() {
+        let mut data: &[u8] = &[0x00, 0x00];
+        let result = Vec::<Signature>::read(&mut data).unwrap();
+        assert_eq!(result, Vec::<Signature>::new());
+        assert!(data.is_empty());
+    }
+
+    #[test]
+    fn vec_signature_write_roundtrip() {
+        let signatures = sample_signatures();
+        let mut buf = Vec::new();
+        signatures.write(&mut buf);
+        let mut data: &[u8] = &buf;
+        let result = Vec::<Signature>::read(&mut data).unwrap();
+        assert_eq!(result, signatures);
+        assert!(data.is_empty());
+    }
+
     // Test vectors from BOLT 1 Appendix A
     // https://github.com/lightning/bolts/blob/master/01-messaging.md#appendix-a-bigsize-test-vectors

What do you think? Not needed?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'm afraid this might make the Vec<u8> read/write much slower, since they would be copying one byte at a time unless rustc can somehow optimize for this case.

I think for now it probably makes sense to just handle these cases manually. If we start getting lots of repetitive code we can do a refactoring later.

@kartikangiras kartikangiras force-pushed the codec-commitment-signed branch from c0dfbac to 35607c7 Compare April 6, 2026 18:09
Copy link
Copy Markdown
Owner

@morehouse morehouse left a comment

Choose a reason for hiding this comment

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

Please squash into one commit.

Comment on lines +21 to +22

pub tlvs: CommitmentSignedTlvs,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The field comment is missing here.

Suggested change
pub tlvs: CommitmentSignedTlvs,
/// Optional TLV extensions
pub tlvs: CommitmentSignedTlvs,

Comment on lines +25 to +34
/// TLV extensions for the `commitment_signed` message.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CommitmentSignedTlvs {}

impl CommitmentSignedTlvs {
/// Extracts commitment signed TLVs from a parsed TLV stream.
fn from_stream(_stream: &TlvStream) -> Self {
Self {}
}
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

A no-op TLV implementation isn't useful. We should implement the funding_txid TLV from the spec.

let mut out = Vec::new();
self.channel_id.write(&mut out);
self.signature.write(&mut out);
(self.htlc_signatures.len() as u16).write(&mut out);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I'm afraid this might make the Vec<u8> read/write much slower, since they would be copying one byte at a time unless rustc can somehow optimize for this case.

I think for now it probably makes sense to just handle these cases manually. If we start getting lots of repetitive code we can do a refactoring later.

/// `funding_signed` message (BOLT 2).
pub const FUNDING_SIGNED: u16 = 35;
/// `commitment_signed` message (BOLT 2).
pub const COMMITMENT_SIGNED: u16 = 132;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

You still haven't addressed the message order here or below.

/// for the commitment transaction and signatures for any pending HTLC outputs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitmentSigned {
/// The channel ID
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
/// The channel ID
/// The channel ID derived from the funding outpoint

///
/// # Errors
///
/// Returns `Truncated` if the payload is too short for any fixed field.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Also can return InvalidSignature and TLV errors.

let mut out = Vec::new();
self.channel_id.write(&mut out);
self.signature.write(&mut out);
#[allow(clippy::cast_possible_truncation)]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggested change
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_truncation)] // HTLC signature count checked above

})
);
}
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

We should add a decode_unknown_odd_tlv_ignored test.

@kartikangiras kartikangiras force-pushed the codec-commitment-signed branch from 326f594 to 88aab2a Compare April 7, 2026 18:42
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.

3 participants