Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions smite/src/bolt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod tx_abort;
mod tx_complete;
mod tx_remove_input;
mod tx_remove_output;
mod tx_signatures;
mod types;
mod warning;
mod wire;
Expand All @@ -41,6 +42,7 @@ pub use tx_abort::TxAbort;
pub use tx_complete::TxComplete;
pub use tx_remove_input::TxRemoveInput;
pub use tx_remove_output::TxRemoveOutput;
pub use tx_signatures::{TxSignatures, TxSignaturesTlvs, Witness};
pub use types::{
BigSize, CHANNEL_ID_SIZE, COMPACT_SIGNATURE_SIZE, ChannelId, MAX_MESSAGE_SIZE, PUBLIC_KEY_SIZE,
TXID_SIZE, Txid,
Expand Down Expand Up @@ -133,6 +135,8 @@ pub mod msg_type {
pub const TX_REMOVE_OUTPUT: u16 = 69;
/// `tx_complete` message (BOLT 2).
pub const TX_COMPLETE: u16 = 70;
/// `tx_signatures` message (BOLT 2).
pub const TX_SIGNATURES: u16 = 71;
/// `tx_abort` message (BOLT 2).
pub const TX_ABORT: u16 = 74;
/// Gossip timestamp filter message (BOLT 7).
Expand Down Expand Up @@ -173,6 +177,8 @@ pub enum Message {
TxRemoveOutput(TxRemoveOutput),
/// `tx_complete` message (type 70).
TxComplete(TxComplete),
/// `tx_signatures` message (type 71).
TxSignatures(TxSignatures),
/// `tx_abort` message (type 74).
TxAbort(TxAbort),
/// Gossip timestamp filter message (type 265).
Expand Down Expand Up @@ -209,6 +215,7 @@ impl Message {
Self::TxRemoveInput(_) => msg_type::TX_REMOVE_INPUT,
Self::TxRemoveOutput(_) => msg_type::TX_REMOVE_OUTPUT,
Self::TxComplete(_) => msg_type::TX_COMPLETE,
Self::TxSignatures(_) => msg_type::TX_SIGNATURES,
Self::TxAbort(_) => msg_type::TX_ABORT,
Self::GossipTimestampFilter(_) => msg_type::GOSSIP_TIMESTAMP_FILTER,
Self::Unknown { msg_type, .. } => *msg_type,
Expand Down Expand Up @@ -236,6 +243,7 @@ impl Message {
Self::TxRemoveInput(m) => out.extend(m.encode()),
Self::TxRemoveOutput(m) => out.extend(m.encode()),
Self::TxComplete(m) => out.extend(m.encode()),
Self::TxSignatures(m) => out.extend(m.encode()),
Self::TxAbort(m) => out.extend(m.encode()),
Self::GossipTimestampFilter(m) => out.extend(m.encode()),
Self::Unknown { payload, .. } => out.extend(payload),
Expand Down Expand Up @@ -270,6 +278,7 @@ impl Message {
msg_type::TX_REMOVE_INPUT => Ok(Self::TxRemoveInput(TxRemoveInput::decode(cursor)?)),
msg_type::TX_REMOVE_OUTPUT => Ok(Self::TxRemoveOutput(TxRemoveOutput::decode(cursor)?)),
msg_type::TX_COMPLETE => Ok(Self::TxComplete(TxComplete::decode(cursor)?)),
msg_type::TX_SIGNATURES => Ok(Self::TxSignatures(TxSignatures::decode(cursor)?)),
msg_type::TX_ABORT => Ok(Self::TxAbort(TxAbort::decode(cursor)?)),
msg_type::GOSSIP_TIMESTAMP_FILTER => Ok(Self::GossipTimestampFilter(
GossipTimestampFilter::decode(cursor)?,
Expand Down Expand Up @@ -597,6 +606,23 @@ mod tests {
assert_eq!(decoded, Message::TxAbort(tx_abort));
}

#[test]
fn message_tx_signatures_roundtrip() {
let tx_sigs = TxSignatures {
channel_id: ChannelId::new([0xef; CHANNEL_ID_SIZE]),
txid: Txid::from_byte_array([0xcc; TXID_SIZE]),
witnesses: vec![
Witness(vec![vec![0xde, 0xad], vec![0xbe, 0xef]]),
Witness(vec![vec![0x01]]),
],
tlvs: TxSignaturesTlvs::default(),
};
let msg = Message::TxSignatures(tx_sigs.clone());
let encoded = msg.encode();
let decoded = Message::decode(&encoded).unwrap();
assert_eq!(decoded, Message::TxSignatures(tx_sigs));
}

#[test]
fn message_unknown_roundtrip() {
let msg = Message::Unknown {
Expand Down Expand Up @@ -672,6 +698,16 @@ mod tests {
.msg_type(),
msg_type::TX_COMPLETE
);
assert_eq!(
Message::TxSignatures(TxSignatures {
channel_id: ChannelId::new([0; CHANNEL_ID_SIZE]),
txid: Txid::from_byte_array([0; TXID_SIZE]),
witnesses: vec![],
tlvs: TxSignaturesTlvs::default(),
})
.msg_type(),
msg_type::TX_SIGNATURES
);
assert_eq!(
Message::TxAbort(TxAbort::new(ChannelId::new([0; CHANNEL_ID_SIZE]), "")).msg_type(),
msg_type::TX_ABORT
Expand Down
270 changes: 270 additions & 0 deletions smite/src/bolt/tx_signatures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
//! BOLT 2 `tx_signatures` message.

use super::BoltError;
use super::tlv::TlvStream;
use super::types::{ChannelId, Txid};
use super::wire::WireFormat;

/// A witness stack for a single transaction input.
///
/// Each inner `Vec<u8>` is a single witness stack item.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Witness(pub Vec<Vec<u8>>);
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.

IIUC, a witness should just be Vec<u8>, not Vec<Vec<u8>>.


/// BOLT 2 `tx_signatures` message (type 71).
///
/// Sent during interactive transaction construction to provide the sender's
/// witnesses for the negotiated transaction. Both peers exchange
/// `tx_signatures` before broadcasting.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxSignatures {
/// The channel ID.
pub channel_id: ChannelId,
/// The transaction ID (little-endian, Bitcoin serialization).
pub txid: Txid,
/// Witnesses for the transaction inputs.
pub witnesses: Vec<Witness>,
/// Optional TLV extensions.
pub tlvs: TxSignaturesTlvs,
}
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 also should implement the tlvs field.


/// TLV extensions for the `tx_signatures` message.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TxSignaturesTlvs {}
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.

Please actually implement the TLVs rather than ignoring them.


impl TxSignatures {
/// Encodes to wire format (without message type prefix).
///
/// # Panics
///
/// Panics if the number of witnesses or the number of items in any witness
/// stack exceeds `u16::MAX`.
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
self.channel_id.write(&mut out);
self.txid.write(&mut out);
assert!(
self.witnesses.len() <= usize::from(u16::MAX),
"witness count exceeds u16::MAX"
);
#[allow(clippy::cast_possible_truncation)]
(self.witnesses.len() as u16).write(&mut out);
for witness in &self.witnesses {
assert!(
witness.0.len() <= usize::from(u16::MAX),
"witness stack item count exceeds u16::MAX"
);
#[allow(clippy::cast_possible_truncation)]
(witness.0.len() as u16).write(&mut out);
for item in &witness.0 {
item.write(&mut out);
}
}

// Encode TLVs
let tlv_stream = TlvStream::new();
out.extend(tlv_stream.encode());

out
}

/// Decodes from wire format (without message type prefix).
///
/// # Errors
///
/// Returns `Truncated` if the payload is too short for any fixed field or
/// declared variable-length data.
pub fn decode(payload: &[u8]) -> Result<Self, BoltError> {
let mut cursor = payload;
let channel_id: ChannelId = WireFormat::read(&mut cursor)?;
let txid: Txid = WireFormat::read(&mut cursor)?;
let num_witnesses: u16 = WireFormat::read(&mut cursor)?;
let mut witnesses = Vec::with_capacity(num_witnesses as usize);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks like recipe for an Out-Of-Memory (OOM) crash in case the fuzzer decides on a high value for num_witnesses or num_items. This would mean the WireFormat::read loop doesn't even get a chance to fail on a truncated payload.

I'd say it's better to let the vector grow dynamically:

let mut witnesses = Vec::new();

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 fuzzer shouldn't be choosing values that get decoded, only values that get encoded.

Either way, num_witnesses is capped at 64K due to u16::MAX. I think it makes sense to preallocate in this case.

for _ in 0..num_witnesses {
let num_items: u16 = WireFormat::read(&mut cursor)?;
let mut stack = Vec::with_capacity(num_items as usize);
for _ in 0..num_items {
let item: Vec<u8> = WireFormat::read(&mut cursor)?;
stack.push(item);
}
witnesses.push(Witness(stack));
}

// Decode TLVs (remaining bytes); no known even types for this message.
let _tlv_stream = TlvStream::decode(cursor)?;
let tlvs = TxSignaturesTlvs {};

Ok(Self {
channel_id,
txid,
witnesses,
tlvs,
})
}
}

#[cfg(test)]
mod tests {
use super::super::{CHANNEL_ID_SIZE, TXID_SIZE};
use super::*;
use secp256k1::hashes::Hash;

fn sample_txid() -> Txid {
Txid::from_byte_array([0xcc; TXID_SIZE])
}

#[test]
fn roundtrip() {
let original = TxSignatures {
channel_id: ChannelId::new([0xab; CHANNEL_ID_SIZE]),
txid: sample_txid(),
witnesses: vec![
Witness(vec![vec![0x01, 0x02], vec![0x03]]),
Witness(vec![vec![0xde, 0xad, 0xbe, 0xef]]),
],
tlvs: TxSignaturesTlvs::default(),
};
let encoded = original.encode();
let decoded = TxSignatures::decode(&encoded).unwrap();
assert_eq!(original, decoded);
}

#[test]
fn roundtrip_empty_witnesses() {
let original = TxSignatures {
channel_id: ChannelId::new([0x00; CHANNEL_ID_SIZE]),
txid: sample_txid(),
witnesses: vec![],
tlvs: TxSignaturesTlvs::default(),
};
let encoded = original.encode();
// channel_id(32) + txid(32) + num_witnesses(2) = 66
assert_eq!(encoded.len(), 66);
let decoded = TxSignatures::decode(&encoded).unwrap();
assert_eq!(original, decoded);
}

#[test]
fn roundtrip_empty_stack_items() {
// A witness with zero stack items is valid wire-wise.
let original = TxSignatures {
channel_id: ChannelId::new([0x11; CHANNEL_ID_SIZE]),
txid: sample_txid(),
witnesses: vec![Witness(vec![]), Witness(vec![])],
tlvs: TxSignaturesTlvs::default(),
};
let encoded = original.encode();
let decoded = TxSignatures::decode(&encoded).unwrap();
assert_eq!(original, decoded);
}

#[test]
fn decode_ignores_unknown_odd_tlv() {
let original = TxSignatures {
channel_id: ChannelId::new([0xff; CHANNEL_ID_SIZE]),
txid: sample_txid(),
witnesses: vec![],
tlvs: TxSignaturesTlvs::default(),
};
let mut encoded = original.encode();
// Append unknown odd TLV: type=3, length=2, value=[0xaa, 0xbb]
encoded.extend_from_slice(&[0x03, 0x02, 0xaa, 0xbb]);
let decoded = TxSignatures::decode(&encoded).unwrap();
assert_eq!(decoded, original);
}

#[test]
fn encode_size() {
// channel_id(32) + txid(32) + num_witnesses(2) + num_items(2) + item_len(2) + item(3) = 73
let msg = TxSignatures {
channel_id: ChannelId::new([0x42; CHANNEL_ID_SIZE]),
txid: sample_txid(),
witnesses: vec![Witness(vec![vec![0xaa, 0xbb, 0xcc]])],
tlvs: TxSignaturesTlvs::default(),
};
let encoded = msg.encode();
assert_eq!(encoded.len(), 32 + 32 + 2 + 2 + 2 + 3);
}

#[test]
fn decode_empty() {
assert_eq!(
TxSignatures::decode(&[]),
Err(BoltError::Truncated {
expected: CHANNEL_ID_SIZE,
actual: 0
})
);
}

#[test]
fn decode_truncated_channel_id() {
assert_eq!(
TxSignatures::decode(&[0x00; 20]),
Err(BoltError::Truncated {
expected: CHANNEL_ID_SIZE,
actual: 20
})
);
}

#[test]
fn decode_truncated_txid() {
let mut data = vec![0xaa; CHANNEL_ID_SIZE];
data.extend_from_slice(&[0x00; 20]); // only 20 bytes of txid
assert_eq!(
TxSignatures::decode(&data),
Err(BoltError::Truncated {
expected: TXID_SIZE,
actual: 20
})
);
}

#[test]
fn decode_truncated_witnesses_count() {
let mut data = vec![0xaa; CHANNEL_ID_SIZE];
data.extend_from_slice(&[0xcc; TXID_SIZE]);
data.push(0x00); // only 1 byte of witnesses count
assert_eq!(
TxSignatures::decode(&data),
Err(BoltError::Truncated {
expected: 2,
actual: 1
})
);
}

#[test]
fn decode_truncated_stack_items_count() {
let mut data = vec![0xaa; CHANNEL_ID_SIZE];
data.extend_from_slice(&[0xcc; TXID_SIZE]);
data.extend_from_slice(&[0x00, 0x01]); // num_witnesses = 1
data.push(0x00); // only 1 byte of stack items count
assert_eq!(
TxSignatures::decode(&data),
Err(BoltError::Truncated {
expected: 2,
actual: 1
})
);
}

#[test]
fn decode_truncated_item_data() {
let mut data = vec![0xaa; CHANNEL_ID_SIZE];
data.extend_from_slice(&[0xcc; TXID_SIZE]);
data.extend_from_slice(&[0x00, 0x01]); // num_witnesses = 1
data.extend_from_slice(&[0x00, 0x01]); // num_stack_items = 1
data.extend_from_slice(&[0x00, 0x05]); // item_len = 5
data.extend_from_slice(&[0x11, 0x22]); // only 2 bytes of item data
assert_eq!(
TxSignatures::decode(&data),
Err(BoltError::Truncated {
expected: 5,
actual: 2
})
);
}
}
Loading