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
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 @@ -124,6 +124,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
23 changes: 22 additions & 1 deletion smite-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@ 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, 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 no reply is awaited before then.
///
/// 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 @@ -571,6 +584,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 @@ -594,7 +608,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(_)
Expand Down Expand Up @@ -742,6 +758,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 @@ -789,6 +806,7 @@ impl Operation {
| Self::SendShutdown
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::MineBlocks(_)
| Self::BroadcastTransaction
| Self::LookupShortChannelId => vec![],
Expand Down Expand Up @@ -840,6 +858,7 @@ impl Operation {
| Self::RecvAcceptChannel
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::MineBlocks(_)
| Self::BroadcastTransaction => true,
}
Expand Down Expand Up @@ -896,6 +915,7 @@ impl Operation {
| Self::RecvAcceptChannel
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::RecvShutdown
| Self::MineBlocks(_)
| Self::BroadcastTransaction
| Self::LookupShortChannelId => false,
Expand Down Expand Up @@ -951,6 +971,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
96 changes: 95 additions & 1 deletion smite-scenarios/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,13 +522,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 @@ -583,6 +584,32 @@ 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]);
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 @@ -684,6 +711,7 @@ impl<C: Connection, B: BitcoinRpc, R: TargetRpc> Executor<C, B, R> {
///
/// This currently includes:
/// - `channel_ready`
/// - `shutdown`
///
/// # Errors
///
Expand Down Expand Up @@ -720,6 +748,20 @@ impl<C: Connection, B: BitcoinRpc, R: TargetRpc> Executor<C, B, R> {
);
Ok(true)
}
Message::Shutdown(sd) => {
log::debug!("received shutdown on {}", sd.channel_id);
// TODO: judge received `shutdown`s with an oracle here. It must reject one:
// * for an untracked channel
// * for a channel whose `funding_signed` we haven't received yet
// * for a channel we already received a `shutdown` on
// * with a `scriptpubkey` in a form the negotiated features disallow
// * with a `scriptpubkey` differing from a non-zero-length upfront shutdown
// script the target sent in `accept_channel`
// * sent while updates are pending on our commitment transaction, once HTLCs
// are supported
record_recv_shutdown(&mut self.channel_states, sd);
Ok(true)
}
_ => Ok(false),
}
}
Expand Down Expand Up @@ -1364,6 +1406,23 @@ 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 {
// TODO: we only expect `shutdown` when all HTLCs are resolved
channel_states.get(&channel_id).is_some_and(|state| {
state.our_shutdown_script.is_some()
&& state.counterparty_shutdown_script.is_none()
&& state.received_channel_ready_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 @@ -1507,6 +1566,41 @@ 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) {
// TODO: a second `shutdown` on the same channel violates BOLT 2 and is dropped
// here unjudged until we have a shutdown oracle
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