From 95030298d8de14951ddacd30e3b5fdc166f3c743 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <86455065+theAnuragMishra@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:42:28 +0000 Subject: [PATCH] bolt: add channel_reestablish codec --- smite/src/bolt.rs | 53 ++++++ smite/src/bolt/channel_reestablish.rs | 234 ++++++++++++++++++++++++++ smite/src/bolt/types.rs | 3 + 3 files changed, 290 insertions(+) create mode 100644 smite/src/bolt/channel_reestablish.rs diff --git a/smite/src/bolt.rs b/smite/src/bolt.rs index d380bc9..7e3fe33 100644 --- a/smite/src/bolt.rs +++ b/smite/src/bolt.rs @@ -5,6 +5,7 @@ mod accept_channel; mod channel_ready; +mod channel_reestablish; mod error; mod funding_created; mod funding_signed; @@ -28,6 +29,7 @@ mod wire; pub use accept_channel::{AcceptChannel, AcceptChannelTlvs}; pub use channel_ready::{ChannelReady, ChannelReadyTlvs}; +pub use channel_reestablish::ChannelReestablish; pub use error::Error; pub use funding_created::FundingCreated; pub use funding_signed::FundingSigned; @@ -127,6 +129,8 @@ pub mod msg_type { pub const TX_ACK_RBF: u16 = 73; /// `tx_abort` message (BOLT 2). pub const TX_ABORT: u16 = 74; + /// `channel_reestablish` message (BOLT 2). + pub const CHANNEL_REESTABLISH: u16 = 136; /// Gossip timestamp filter message (BOLT 7). pub const GOSSIP_TIMESTAMP_FILTER: u16 = 265; } @@ -171,6 +175,8 @@ pub enum Message { TxAckRbf(TxAckRbf), /// `tx_abort` message (type 74). TxAbort(TxAbort), + /// `channel_reestablish` message (type 136). + ChannelReestablish(ChannelReestablish), /// Gossip timestamp filter message (type 265). GossipTimestampFilter(GossipTimestampFilter), /// Unknown message type. @@ -208,6 +214,7 @@ impl Message { Self::TxInitRbf(_) => msg_type::TX_INIT_RBF, Self::TxAckRbf(_) => msg_type::TX_ACK_RBF, Self::TxAbort(_) => msg_type::TX_ABORT, + Self::ChannelReestablish(_) => msg_type::CHANNEL_REESTABLISH, Self::GossipTimestampFilter(_) => msg_type::GOSSIP_TIMESTAMP_FILTER, Self::Unknown { msg_type, .. } => *msg_type, } @@ -237,6 +244,7 @@ impl Message { Self::TxInitRbf(m) => out.extend(m.encode()), Self::TxAckRbf(m) => out.extend(m.encode()), Self::TxAbort(m) => out.extend(m.encode()), + Self::ChannelReestablish(m) => out.extend(m.encode()), Self::GossipTimestampFilter(m) => out.extend(m.encode()), Self::Unknown { payload, .. } => out.extend(payload), } @@ -273,6 +281,9 @@ impl Message { msg_type::TX_INIT_RBF => Ok(Self::TxInitRbf(TxInitRbf::decode(cursor)?)), msg_type::TX_ACK_RBF => Ok(Self::TxAckRbf(TxAckRbf::decode(cursor)?)), msg_type::TX_ABORT => Ok(Self::TxAbort(TxAbort::decode(cursor)?)), + msg_type::CHANNEL_REESTABLISH => Ok(Self::ChannelReestablish( + ChannelReestablish::decode(cursor)?, + )), msg_type::GOSSIP_TIMESTAMP_FILTER => Ok(Self::GossipTimestampFilter( GossipTimestampFilter::decode(cursor)?, )), @@ -309,6 +320,7 @@ mod tests { use secp256k1::hashes::Hash; use secp256k1::{PublicKey, Secp256k1, SecretKey}; use types::CHAIN_HASH_SIZE; + use types::PER_COMMITMENT_SECRET_SIZE; // Tests ordered by message type number: Warning(1), Init(16), Error(17), Ping(18), Pong(19) @@ -615,6 +627,28 @@ mod tests { assert_eq!(decoded, Message::TxAbort(tx_abort)); } + #[test] + fn message_channel_reestablish_roundtrip() { + let secp = Secp256k1::new(); + let sk = + SecretKey::from_byte_array([0x11; PER_COMMITMENT_SECRET_SIZE]).expect("valid secret"); + let pk = PublicKey::from_secret_key(&secp, &sk); + + let channel_reestablish = ChannelReestablish { + channel_id: ChannelId::new([0xcd; CHANNEL_ID_SIZE]), + next_commitment_number: 42, + next_revocation_number: 41, + your_last_per_commitment_secret: [0xab; PER_COMMITMENT_SECRET_SIZE], + my_current_per_commitment_point: pk, + tlvs: TlvStream::new(), + }; + + let msg = Message::ChannelReestablish(channel_reestablish.clone()); + let encoded = msg.encode(); + let decoded = Message::decode(&encoded).unwrap(); + assert_eq!(decoded, Message::ChannelReestablish(channel_reestablish)); + } + #[test] fn message_gossip_timestamp_filter_roundtrip() { let chain_hash = [0x6f; 32]; @@ -637,6 +671,7 @@ mod tests { } #[test] + #[allow(clippy::too_many_lines)] fn message_type_values() { assert_eq!( Message::Warning(Warning::all_channels("")).msg_type(), @@ -722,6 +757,24 @@ mod tests { Message::TxAbort(TxAbort::new(ChannelId::new([0; CHANNEL_ID_SIZE]), "")).msg_type(), msg_type::TX_ABORT ); + assert_eq!( + Message::ChannelReestablish(ChannelReestablish { + channel_id: ChannelId::new([0; CHANNEL_ID_SIZE]), + next_commitment_number: 0, + next_revocation_number: 0, + your_last_per_commitment_secret: [0; PER_COMMITMENT_SECRET_SIZE], + // Taken from https://github.com/lightning/bolts/blob/master/03-transactions.md#appendix-e-key-derivation-test-vectors + my_current_per_commitment_point: PublicKey::from_slice(&[ + 0x02, 0x5f, 0x71, 0x17, 0xa7, 0x81, 0x50, 0xfe, 0x2e, 0xf9, 0x7d, 0xb7, 0xcf, + 0xc8, 0x3b, 0xd5, 0x7b, 0x2e, 0x2c, 0x0d, 0x0d, 0xd2, 0x5e, 0xaf, 0x46, 0x7a, + 0x4a, 0x1c, 0x2a, 0x45, 0xce, 0x14, 0x86, + ]) + .unwrap(), + tlvs: TlvStream::new(), + }) + .msg_type(), + msg_type::CHANNEL_REESTABLISH + ); assert_eq!( Message::GossipTimestampFilter(GossipTimestampFilter::no_gossip([0u8; 32])).msg_type(), msg_type::GOSSIP_TIMESTAMP_FILTER diff --git a/smite/src/bolt/channel_reestablish.rs b/smite/src/bolt/channel_reestablish.rs new file mode 100644 index 0000000..60afbac --- /dev/null +++ b/smite/src/bolt/channel_reestablish.rs @@ -0,0 +1,234 @@ +//! BOLT 2 `channel_reestablish` message. + +use super::BoltError; +use super::tlv::TlvStream; +use super::types::ChannelId; +use super::types::PER_COMMITMENT_SECRET_SIZE; +use super::wire::WireFormat; +use secp256k1::PublicKey; + +/// BOLT 2 `channel_reestablish` message (type 136). +/// +/// Sent after reconnecting to synchronize commitment/revocation counters and +/// reestablish a channel's state. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChannelReestablish { + /// The channel ID. + pub channel_id: ChannelId, + /// The next commitment number expected from the peer. + pub next_commitment_number: u64, + /// The next revocation number expected from the peer. + pub next_revocation_number: u64, + /// The sender's view of the receiver's last per-commitment secret. + pub your_last_per_commitment_secret: [u8; PER_COMMITMENT_SECRET_SIZE], + /// The sender's current per-commitment point. + pub my_current_per_commitment_point: PublicKey, + /// Optional TLV extensions. + pub tlvs: TlvStream, +} + +impl ChannelReestablish { + /// Encodes to wire format (without message type prefix). + #[must_use] + pub fn encode(&self) -> Vec { + let mut out = Vec::new(); + self.channel_id.write(&mut out); + self.next_commitment_number.write(&mut out); + self.next_revocation_number.write(&mut out); + self.your_last_per_commitment_secret.write(&mut out); + self.my_current_per_commitment_point.write(&mut out); + out.extend(self.tlvs.encode()); + + out + } + + /// Decodes from wire format (without message type prefix). + /// + /// # Errors + /// + /// Returns `Truncated` if the payload is too short for any fixed field or + /// `InvalidPublicKey` if the per-commitment point is invalid. + pub fn decode(payload: &[u8]) -> Result { + let mut cursor = payload; + + let channel_id = WireFormat::read(&mut cursor)?; + let next_commitment_number = WireFormat::read(&mut cursor)?; + let next_revocation_number = WireFormat::read(&mut cursor)?; + let your_last_per_commitment_secret = WireFormat::read(&mut cursor)?; + let my_current_per_commitment_point = WireFormat::read(&mut cursor)?; + let tlvs = TlvStream::decode(cursor)?; + + Ok(Self { + channel_id, + next_commitment_number, + next_revocation_number, + your_last_per_commitment_secret, + my_current_per_commitment_point, + tlvs, + }) + } +} + +#[cfg(test)] +mod tests { + use super::super::{CHANNEL_ID_SIZE, PUBLIC_KEY_SIZE}; + use super::*; + use secp256k1::{Secp256k1, SecretKey}; + + /// Valid `ChannelReestablish` message for testing. + fn sample_channel_reestablish() -> ChannelReestablish { + let secp = Secp256k1::new(); + let sk = SecretKey::from_byte_array([0x11; 32]).expect("valid secret"); + let pk = PublicKey::from_secret_key(&secp, &sk); + + ChannelReestablish { + channel_id: ChannelId::new([0xaa; CHANNEL_ID_SIZE]), + next_commitment_number: 42, + next_revocation_number: 41, + your_last_per_commitment_secret: [0xbb; PER_COMMITMENT_SECRET_SIZE], + my_current_per_commitment_point: pk, + tlvs: TlvStream::new(), + } + } + + #[test] + fn encode_fixed_field_size() { + let msg = sample_channel_reestablish(); + let encoded = msg.encode(); + // channel_id(32) + next_commitment_number(8) + next_revocation_number(8) + // + your_last_per_commitment_secret(32) + my_current_per_commitment_point(33) = 113 + assert_eq!(encoded.len(), 113); + } + + #[test] + fn roundtrip() { + let original = sample_channel_reestablish(); + let encoded = original.encode(); + let decoded = ChannelReestablish::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn roundtrip_with_tlvs() { + let mut original = sample_channel_reestablish(); + original.tlvs.add(3, vec![0xaa, 0xbb]); + + let encoded = original.encode(); + let decoded = ChannelReestablish::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + + #[test] + fn decode_unknown_odd_tlv_ignored() { + let msg = sample_channel_reestablish(); + let mut encoded = msg.encode(); + + // Append unknown odd TLV: type 5, length 2, value [0xaa, 0xbb] + encoded.extend_from_slice(&[0x05, 0x02, 0xaa, 0xbb]); + + let decoded = ChannelReestablish::decode(&encoded).unwrap(); + assert_eq!(decoded.tlvs.get(5), Some(&[0xaa, 0xbb][..])); + } + + #[test] + fn decode_unknown_even_tlv_rejected() { + let msg = sample_channel_reestablish(); + let mut encoded = msg.encode(); + + // Append unknown even TLV: type 4, length 1, value [0x00] + encoded.extend_from_slice(&[0x04, 0x01, 0x00]); + + assert!(matches!( + ChannelReestablish::decode(&encoded), + Err(BoltError::TlvUnknownEvenType(4)) + )); + } + + #[test] + fn decode_truncated_channel_id() { + assert_eq!( + ChannelReestablish::decode(&[0x00; 20]), + Err(BoltError::Truncated { + expected: CHANNEL_ID_SIZE, + actual: 20 + }) + ); + } + + #[test] + fn decode_truncated_next_commitment_number() { + // channel_id(32) + only 2 bytes into next_commitment_number + let mut data = vec![0xaa; CHANNEL_ID_SIZE]; + data.extend_from_slice(&[0x00; 2]); + assert_eq!( + ChannelReestablish::decode(&data), + Err(BoltError::Truncated { + expected: 8, + actual: 2 + }) + ); + } + + #[test] + fn decode_truncated_next_revocation_number() { + // channel_id(32) + next_commitment_number(8) + only 3 bytes into next_revocation_number + let mut data = vec![0xaa; CHANNEL_ID_SIZE]; + data.extend_from_slice(&42u64.to_be_bytes()); + data.extend_from_slice(&[0x00; 3]); + assert_eq!( + ChannelReestablish::decode(&data), + Err(BoltError::Truncated { + expected: 8, + actual: 3 + }) + ); + } + + #[test] + fn decode_truncated_your_last_per_commitment_secret() { + // channel_id(32) + next_commitment_number(8) + next_revocation_number(8) + // + only 10 bytes of your_last_per_commitment_secret + let mut data = vec![0xaa; CHANNEL_ID_SIZE]; + data.extend_from_slice(&42u64.to_be_bytes()); + data.extend_from_slice(&41u64.to_be_bytes()); + data.extend_from_slice(&[0xbb; 10]); + assert_eq!( + ChannelReestablish::decode(&data), + Err(BoltError::Truncated { + expected: PER_COMMITMENT_SECRET_SIZE, + actual: 10 + }) + ); + } + + #[test] + fn decode_truncated_my_current_per_commitment_point() { + // Fixed fields + only 10 bytes of point data + let mut data = vec![0xaa; CHANNEL_ID_SIZE]; + data.extend_from_slice(&42u64.to_be_bytes()); + data.extend_from_slice(&41u64.to_be_bytes()); + data.extend_from_slice(&[0xbb; PER_COMMITMENT_SECRET_SIZE]); + data.extend_from_slice(&[0x02; 10]); + assert_eq!( + ChannelReestablish::decode(&data), + Err(BoltError::Truncated { + expected: PUBLIC_KEY_SIZE, + actual: 10 + }) + ); + } + + #[test] + fn decode_invalid_my_current_per_commitment_point() { + // Full length payload with invalid all-zero compressed key bytes. + let mut data = vec![0xaa; CHANNEL_ID_SIZE]; + data.extend_from_slice(&42u64.to_be_bytes()); + data.extend_from_slice(&41u64.to_be_bytes()); + data.extend_from_slice(&[0xbb; PER_COMMITMENT_SECRET_SIZE]); + data.extend_from_slice(&[0x00; PUBLIC_KEY_SIZE]); + assert_eq!( + ChannelReestablish::decode(&data), + Err(BoltError::InvalidPublicKey([0x00; PUBLIC_KEY_SIZE])) + ); + } +} diff --git a/smite/src/bolt/types.rs b/smite/src/bolt/types.rs index 9ca808a..2a6b792 100644 --- a/smite/src/bolt/types.rs +++ b/smite/src/bolt/types.rs @@ -20,6 +20,9 @@ pub const COMPACT_SIGNATURE_SIZE: usize = 64; /// Size of a compressed secp256k1 public key. pub const PUBLIC_KEY_SIZE: usize = 33; +/// Size of `your_last_per_commitment_secret` in bytes. +pub const PER_COMMITMENT_SECRET_SIZE: usize = 32; + /// A 32-byte channel identifier. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct ChannelId(pub [u8; CHANNEL_ID_SIZE]);