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
102 changes: 61 additions & 41 deletions smite-scenarios/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ use smite::channel_tx::{
build_funding_transaction,
};
use smite::noise::{ConnectionError, NoiseConnection};
use smite::oracles::{AcceptChannelContext, AcceptChannelOracle, Oracle};
use smite::oracles::{
AcceptChannelContext, AcceptChannelOracle, FundingSignedContext, FundingSignedOracle, Oracle,
};
use smite::pending_channel::PendingChannel;
use smite::violation::Violation;
use smite_ir::operation::AcceptChannelField;
Expand Down Expand Up @@ -491,7 +493,10 @@ impl<C: Connection, B: BitcoinRpc> Executor<C, B> {
log::debug!("[{:?}] RecvFundingSigned: waiting", start.elapsed());
let fs = recv_funding_signed(&mut self.conn)?;
log::debug!("[{:?}] RecvFundingSigned: received", start.elapsed());
verify_funding_signed(&fs, &self.channel_states)?;
FundingSignedOracle.evaluate(&FundingSignedContext {
funding_signed: &fs,
channel: self.channel_states.get(&fs.channel_id),
})?;
Some(Variable::ChannelId(fs.channel_id))
}

Expand Down Expand Up @@ -960,6 +965,10 @@ fn build_funding_created(
&accept_channel.funding_pubkey,
open_channel.funding_satoshis,
);
// TODO: Once we support sending malformed signatures, update this state
// when constructing one so that the peer's acceptance can be detected as a
// violation.
let sent_invalid_signature = opener_funding_pubkey != open_channel.funding_pubkey;

// Building the same message again must not clobber a channel whose state
// has already been established (and possibly advanced).
Expand All @@ -970,6 +979,7 @@ fn build_funding_created(
state,
is_funding_outpoint_valid,
mined_txids.contains(&funding_outpoint.txid),
sent_invalid_signature,
)
});

Expand Down Expand Up @@ -1316,29 +1326,6 @@ fn is_channel_ready_expected(
})
}

/// Verifies the counterparty's signature from a `funding_signed` message using
/// the channel state associated with the message's `channel_id`.
///
/// # Errors
///
/// Returns [`Violation::UnknownChannel`] if no channel state exists for the
/// given `channel_id`, or [`Violation::InvalidCounterpartySignature`] if the
/// signature is invalid for the holder's initial commitment transaction.
fn verify_funding_signed(
fs: &FundingSigned,
channel_states: &HashMap<ChannelId, ChannelState>,
) -> Result<(), Violation> {
let state = channel_states
.get(&fs.channel_id)
.ok_or(Violation::UnknownChannel(fs.channel_id))?;

state
.config
.verify_counterparty_signature(&state.commitment, &state.holder, &fs.signature)
.then_some(())
.ok_or(Violation::InvalidCounterpartySignature(fs.channel_id))
}

/// Records a sent `open_channel`, keyed by `temporary_channel_id`, so the
/// funding flow can build commitments from the values actually put on the wire.
///
Expand Down Expand Up @@ -3597,10 +3584,13 @@ mod tests {
std::time::Instant::now(),
)
.unwrap_err();
assert!(matches!(
err,
ExecuteError::Violation(Violation::UnknownChannel(id)) if id == channel_id
));
let ExecuteError::Violation(Violation::InvalidFundingSigned(id, reason)) = &err else {
panic!("unexpected error: {err:?}");
};
assert_eq!(*id, channel_id);
assert!(
reason.contains("unknown channel_id: no funding_created was sent for this channel")
);
}

#[test]
Expand Down Expand Up @@ -3637,10 +3627,11 @@ mod tests {
std::time::Instant::now(),
)
.unwrap_err();
assert!(matches!(
err,
ExecuteError::Violation(Violation::InvalidCounterpartySignature(id)) if id == channel_id
));
let ExecuteError::Violation(Violation::InvalidFundingSigned(id, reason)) = &err else {
panic!("unexpected error: {err:?}");
};
assert_eq!(*id, channel_id);
assert!(reason.contains("invalid funding_signed: signature is not valid"));
}

#[test]
Expand Down Expand Up @@ -3866,17 +3857,41 @@ mod tests {

#[test]
fn execute_recv_channel_ready_invalid_funding_outpoint_is_noop() {
let (mut executor, channel_id, _) = recv_channel_ready_executor();
let channel_id = ChannelId::v1_from_funding_outpoint(OutPoint {
txid: "b86cbf63856be414fb9636160d729a29935fb998524410475878295272021727"
.parse()
.unwrap(),
vout: 0,
});

// The expected signature here was computed using LDK as the source of
// truth.
let fs_bytes = Message::FundingSigned(FundingSigned {
channel_id,
signature: "30440220096b230642dcafeb2271e80b66eb7e7d7b5750af43a253d103bda600d27c10d302206b535c31d1c2a810f87950d17ed368cca017cdce6a26b23608da2fb62e9d8015".parse().unwrap(),
})
.encode();
let cr_bytes = Message::ChannelReady(ChannelReady {
channel_id,
second_per_commitment_point: sample_pubkey(1),
tlvs: ChannelReadyTlvs::default(),
})
.encode();

// Corrupt the negotiated opener funding pubkey so the broadcast funding
// transaction's output no longer pays the negotiated 2-of-2 script,
// marking the funding outpoint invalid.
let mut executor = Executor::new(
MockConnection::new(),
MockBitcoinCli {
utxos: vec![sample_utxo()],
change_spk: sample_change_spk(),
..Default::default()
},
sample_context(),
);
executor.conn.queue_recv(fs_bytes);
executor.conn.queue_recv(cr_bytes);
executor
.negotiations
.get_mut(&ChannelId::new([0xbb; 32]))
.unwrap()
.open_channel
.funding_pubkey = sample_pubkey(1);
.insert(ChannelId::new([0xbb; 32]), sample_funding_negotiation());

let mut instrs = send_funding_created_and_recv_funding_signed_instructions();
instrs.extend([
Expand All @@ -3890,6 +3905,11 @@ mod tests {
},
]);

// We will create a transaction with the wrong acceptor funding pubkey,
// so the broadcast funding transaction's output no longer pays the
// negotiated 2-of-2 script, making the funding outpoint invalid.
instrs[3].inputs = vec![0];

// With invalid funding outpoint the target does not owe us a
// `channel_ready`, so `RecvChannelReady` must be a no-op.
executor
Expand Down
8 changes: 8 additions & 0 deletions smite/src/channel_tx/commitment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ pub struct ChannelState {
/// after the block height at which they receive `funding_created`, so they
/// may never observe it and never send `channel_ready`.
pub was_funding_mined_prematurely: bool,
/// Whether we have ever sent a signature the peer was required to reject,
/// such as a `funding_created`, `commitment_signed`, or HTLC signature etc.
/// it cannot verify. Set on the first occurrence and never cleared, since
/// BOLT 2 requires the peer to fail the channel or disconnect in response,
/// any subsequent positive response is therefore a violation.
pub sent_invalid_signature: bool,
}

impl Side {
Expand Down Expand Up @@ -179,6 +185,7 @@ impl ChannelState {
commitment: CommitmentState,
is_funding_outpoint_valid: bool,
was_funding_mined_prematurely: bool,
sent_invalid_signature: bool,
) -> Self {
Self {
config,
Expand All @@ -188,6 +195,7 @@ impl ChannelState {
acceptor_next_per_commitment_point: None,
is_funding_outpoint_valid,
was_funding_mined_prematurely,
sent_invalid_signature,
}
}

Expand Down
2 changes: 2 additions & 0 deletions smite/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
//! Oracles evaluate conditions beyond simple crashes.

mod accept_channel;
mod funding_signed;

use super::violation::Violation;
pub use accept_channel::{AcceptChannelContext, AcceptChannelOracle};
pub use funding_signed::{FundingSignedContext, FundingSignedOracle};

/// `Oracle` evaluates a condition against some context
pub trait Oracle<C> {
Expand Down
Loading