Skip to content
Draft
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
1 change: 1 addition & 0 deletions smite-ir/src/mutators/operation_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool {
| Operation::RecvAcceptChannel
| Operation::RecvFundingSigned
| Operation::RecvChannelReady
| Operation::RecvShutdown
| Operation::BroadcastTransaction
| Operation::LookupShortChannelId => {
unreachable!("is_param_mutable returned true for {op:?}")
Expand Down
21 changes: 20 additions & 1 deletion smite-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,17 @@ pub enum Operation {
/// point unknown) and its funding transaction has enough confirmations for
/// the target to have sent `channel_ready`.
RecvChannelReady,
/// Receive and parse a `shutdown` from the target.
/// Produces the target's `scriptpubkey` (`Bytes`), or empty `Bytes` if no
/// `shutdown` was received.
///
/// This is a no-op unless we sent a `shutdown` on the channel and the
/// target has not replied there yet.
///
/// Inputs (2):
/// 0: `SentShutdown` from the `SendShutdown` being answered
/// 1: `channel_id` (`ChannelId`) the reply is expected on
RecvShutdown,
/// Mines the given number of blocks on the Bitcoin network.
MineBlocks(u8),
/// Sign wallet inputs of the transaction and broadcast it via `bitcoin-cli`.
Expand Down Expand Up @@ -557,6 +568,7 @@ impl fmt::Display for Operation {
Self::RecvAcceptChannel => write!(f, "RecvAcceptChannel"),
Self::RecvFundingSigned => write!(f, "RecvFundingSigned"),
Self::RecvChannelReady => write!(f, "RecvChannelReady()"),
Self::RecvShutdown => write!(f, "RecvShutdown"),
Self::MineBlocks(v) => write!(f, "MineBlocks({v})"),
Self::BroadcastTransaction => write!(f, "BroadcastTransaction"),
Self::LookupShortChannelId => write!(f, "LookupShortChannelId"),
Expand All @@ -580,7 +592,9 @@ impl Operation {
Self::LoadForwardingFee(_) => Some(VariableType::ForwardingFee),
Self::LoadU16(_) => Some(VariableType::U16),
Self::LoadU8(_) => Some(VariableType::U8),
Self::LoadBytes(_) | Self::LoadShutdownScript(_) => Some(VariableType::Bytes),
Self::LoadBytes(_) | Self::LoadShutdownScript(_) | Self::RecvShutdown => {
Some(VariableType::Bytes)
}
Self::LoadFeatures(_) | Self::LoadChannelType(_) => Some(VariableType::Features),
Self::LoadPrivateKey(_) => Some(VariableType::PrivateKey),
Self::LoadChannelId(_) | Self::RecvFundingSigned => Some(VariableType::ChannelId),
Expand Down Expand Up @@ -720,6 +734,7 @@ impl Operation {
],
Self::RecvAcceptChannel => vec![VariableType::SentOpenChannel],
Self::RecvFundingSigned => vec![VariableType::SentFundingCreated],
Self::RecvShutdown => vec![VariableType::SentShutdown, VariableType::ChannelId],
Self::BroadcastTransaction | Self::LookupShortChannelId => {
vec![VariableType::FundingTransaction]
}
Expand Down Expand Up @@ -765,6 +780,7 @@ impl Operation {
| Self::SendShutdown
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::MineBlocks(_)
| Self::BroadcastTransaction
| Self::LookupShortChannelId => vec![],
Expand Down Expand Up @@ -814,6 +830,7 @@ impl Operation {
| Self::RecvAcceptChannel
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::MineBlocks(_)
| Self::BroadcastTransaction => true,
}
Expand Down Expand Up @@ -868,6 +885,7 @@ impl Operation {
| Self::RecvAcceptChannel
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::MineBlocks(_)
| Self::BroadcastTransaction
| Self::LookupShortChannelId => false,
Expand Down Expand Up @@ -921,6 +939,7 @@ impl Operation {
| Self::RecvAcceptChannel
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::BroadcastTransaction
| Self::LookupShortChannelId => false,
}
Expand Down
7 changes: 6 additions & 1 deletion smite-ir/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ fn display_send_and_recv_channel_ready_program() {
}

#[test]
fn display_send_shutdown_program() {
fn display_send_and_recv_shutdown_program() {
let instructions = vec![
Instruction {
operation: Operation::LoadChannelId([0xcd; 32]),
Expand All @@ -584,6 +584,10 @@ fn display_send_shutdown_program() {
operation: Operation::SendShutdown,
inputs: vec![0, 1],
},
Instruction {
operation: Operation::RecvShutdown,
inputs: vec![2, 0],
},
];

let program = Program { instructions };
Expand All @@ -596,6 +600,7 @@ fn display_send_shutdown_program() {
format!("v0 = LoadChannelId(0x{cid_hex})"),
format!("v1 = LoadShutdownScript(P2wpkh(0x{spk_hex}))"),
"v2 = SendShutdown(v0, v1)".into(),
"v3 = RecvShutdown(v2, v0)".into(),
];
assert_eq!(lines.len(), expected.len(), "line count mismatch");
for (i, (got, want)) in lines.iter().zip(expected.iter()).enumerate() {
Expand Down
107 changes: 102 additions & 5 deletions smite-scenarios/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use smite::channel_tx::{
use smite::noise::{ConnectionError, NoiseConnection};
use smite::oracles::{
AcceptChannelContext, AcceptChannelOracle, ChannelReadyContext, ChannelReadyOracle,
FundingSignedContext, FundingSignedOracle, Oracle,
FundingSignedContext, FundingSignedOracle, Oracle, ShutdownContext, ShutdownOracle,
};
use smite::pending_channel::{KeyOrigin, PendingChannel};
use smite::violation::Violation;
Expand Down Expand Up @@ -507,13 +507,14 @@ impl<C: Connection, B: BitcoinRpc, R: TargetRpc> Executor<C, B, R> {

Operation::SendShutdown => {
let sd = build_shutdown(&variables, &instr.inputs);
let encoded = Message::Shutdown(sd).encode();
let encoded = Message::Shutdown(sd.clone()).encode();
log::debug!(
"[{:?}] SendShutdown: {} bytes",
start.elapsed(),
encoded.len()
);
self.conn.send_message(&encoded)?;
record_send_shutdown(&mut self.channel_states, &sd);
Some(Variable::SentShutdown)
}

Expand Down Expand Up @@ -568,6 +569,34 @@ impl<C: Connection, B: BitcoinRpc, R: TargetRpc> Executor<C, B, R> {
None
}

Operation::RecvShutdown => {
consume_affine(
&mut variables,
instr.inputs[0],
instr.operation.input_types()[0],
);
let channel_id = resolve_channel_id(&variables, instr.inputs[1]);
// TODO: we only expect `shutdown` when all HTLCs are resolved, else this is a
// no-op.
if is_shutdown_expected(&self.channel_states, channel_id) {
log::debug!("[{:?}] RecvShutdown: waiting", start.elapsed());
// `shutdown` is handled implicitly, so one for another channel is
// recorded by `handle_implicit` and we keep waiting for ours.
while is_shutdown_expected(&self.channel_states, channel_id) {
self.recv_bolt::<Shutdown>(RECV_IDLE_TIMEOUT)?;
}
log::debug!("[{:?}] RecvShutdown: received", start.elapsed());
}
// Read the reply from the channel's state, so one recorded before this
// instruction ran is reported just the same.
let scriptpubkey = self
.channel_states
.get(&channel_id)
.and_then(|state| state.counterparty_shutdown_script.clone())
.unwrap_or_default();
Some(Variable::Bytes(scriptpubkey))
}

Operation::MineBlocks(v) => {
// Clear the private mempool and mine the requested blocks,
// adding those transactions to the first block.
Expand Down Expand Up @@ -669,6 +698,7 @@ impl<C: Connection, B: BitcoinRpc, R: TargetRpc> Executor<C, B, R> {
///
/// This currently includes:
/// - `channel_ready`
/// - `shutdown`
///
/// # Errors
///
Expand Down Expand Up @@ -705,6 +735,16 @@ impl<C: Connection, B: BitcoinRpc, R: TargetRpc> Executor<C, B, R> {
);
Ok(true)
}
Message::Shutdown(sd) => {
log::debug!("received shutdown on {}", sd.channel_id);
ShutdownOracle.evaluate(&ShutdownContext {
shutdown: sd,
channel: self.channel_states.get(&sd.channel_id),
negotiated_features: &self.context.negotiated_features,
})?;
record_recv_shutdown(&mut self.channel_states, sd);
Ok(true)
}
_ => Ok(false),
}
}
Expand Down Expand Up @@ -780,8 +820,12 @@ define_resolver!(
);

/// Consumes an affine variable, leaving its slot void so it cannot be used
/// again.
fn consume_affine(variables: &mut [Option<Variable>], index: usize, expected: VariableType) {
/// again, and returns it.
fn consume_affine(
variables: &mut [Option<Variable>],
index: usize,
expected: VariableType,
) -> Variable {
assert!(
expected.is_affine(),
"consume_affine called with non-affine type {expected:?}; voiding the slot would break later reads"
Expand All @@ -790,7 +834,9 @@ fn consume_affine(variables: &mut [Option<Variable>], index: usize, expected: Va
if actual != expected {
type_mismatch(index, expected, actual);
}
variables[index] = None;
variables[index]
.take()
.expect("resolve checked the slot is not void")
}

// -- Operation handlers --
Expand Down Expand Up @@ -1008,6 +1054,8 @@ fn build_funding_created(
is_funding_outpoint_valid,
mined_txids.contains(&funding_outpoint.txid),
sent_invalid_signature,
open_channel.tlvs.upfront_shutdown_script.clone(),
accept_channel.tlvs.upfront_shutdown_script.clone(),
)
});
}
Expand Down Expand Up @@ -1349,6 +1397,22 @@ fn is_channel_ready_expected(
})
}

/// Returns `true` if we sent a `shutdown` on the given channel, the target has
/// sent its `channel_ready` there, and it has not replied to our `shutdown` yet.
///
/// A target that has not sent `channel_ready` may ignore our `shutdown`, so we
/// do not wait for a reply before then.
fn is_shutdown_expected(
channel_states: &HashMap<ChannelId, ChannelState>,
channel_id: ChannelId,
) -> bool {
channel_states.get(&channel_id).is_some_and(|state| {
state.our_shutdown_script.is_some()
&& state.counterparty_shutdown_script.is_none()
&& state.next_counterparty_per_commitment_point().is_some()
})
}

/// 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 @@ -1492,6 +1556,39 @@ fn record_recv_channel_ready(
);
}

/// Records a sent `shutdown`'s `scriptpubkey` in its channel's state, which is
/// what makes a reply expected there.
///
/// The first one is kept: a target may ignore any `shutdown` after the first,
/// so our first one is what it is replying to. A `shutdown` for a channel we do
/// not track is not recorded, leaving no reply expected for it.
fn record_send_shutdown(
channel_states: &mut HashMap<ChannelId, ChannelState>,
shutdown: &Shutdown,
) {
if let Some(state) = channel_states.get_mut(&shutdown.channel_id) {
state
.our_shutdown_script
.get_or_insert_with(|| shutdown.scriptpubkey.clone());
}
}

/// Records a received `shutdown`'s `scriptpubkey` in its channel's state.
///
/// The first one is kept: a target may ignore any `shutdown` after the first,
/// so its first reply is the one a later `RecvShutdown` must report. A
/// `shutdown` for a channel we do not track is not recorded.
fn record_recv_shutdown(
channel_states: &mut HashMap<ChannelId, ChannelState>,
shutdown: &Shutdown,
) {
if let Some(state) = channel_states.get_mut(&shutdown.channel_id) {
state
.counterparty_shutdown_script
.get_or_insert_with(|| shutdown.scriptpubkey.clone());
}
}

/// Extracts a field from a parsed `accept_channel` message.
fn extract_field(ac: &AcceptChannel, field: AcceptChannelField) -> Variable {
match field {
Expand Down
Loading
Loading