Skip to content
Merged
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 @@ -102,6 +102,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool {
| Operation::LoadChainHashFromContext
| Operation::BuildOpenChannel
| Operation::BuildChannelAnnouncement
| Operation::BuildChannelUpdate
| Operation::SendMessage
| Operation::SendOpenChannel
| Operation::RecvAcceptChannel
Expand Down
45 changes: 41 additions & 4 deletions smite-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,25 @@ pub enum Operation {
/// 32-byte node alias, zero-padded.
alias: [u8; 32],
},
/// Build a `channel_update` message (BOLT 7, type 258).
///
/// The signature is computed internally over the double-SHA256 of the
/// message body following the signature field, using the supplied node
/// secret key (per BOLT 7).
///
/// Inputs (11):
/// 0: `node_sk` (`PrivateKey`) -- signs the body
/// 1: `chain_hash` (`ChainHash`)
/// 2: `short_channel_id` (`ShortChannelId`)
/// 3: `timestamp` (`Timestamp`)
/// 4: `message_flags` (`U8`)
/// 5: `channel_flags` (`U8`)
/// 6: `cltv_expiry_delta` (`U16`)
/// 7: `htlc_minimum_msat` (`Amount`)
/// 8: `fee_base_msat` (`ForwardingFee`)
/// 9: `fee_proportional_millionths` (`ForwardingFee`)
/// 10: `htlc_maximum_msat` (`Amount`)
BuildChannelUpdate,

// -- Act: side effects against the target --
/// Send an encoded message over the connection.
Expand Down Expand Up @@ -597,6 +616,7 @@ impl fmt::Display for Operation {
format_hex(rgb_color),
format_hex(alias),
),
Self::BuildChannelUpdate => write!(f, "BuildChannelUpdate"),
Self::SendMessage => write!(f, "SendMessage"),
Self::SendOpenChannel => write!(f, "SendOpenChannel"),
}
Expand Down Expand Up @@ -626,9 +646,9 @@ impl Operation {
Self::ExtractAcceptChannel(field) => Some(field.output_type()),
Self::CreateFundingTransaction => Some(VariableType::FundingTransaction),
Self::BuildOpenChannel => Some(VariableType::OpenChannelMessage),
Self::BuildChannelAnnouncement | Self::BuildNodeAnnouncement { .. } => {
Some(VariableType::Message)
}
Self::BuildChannelAnnouncement
| Self::BuildNodeAnnouncement { .. }
| Self::BuildChannelUpdate => Some(VariableType::Message),
Self::SendMessage | Self::MineBlocks(_) | Self::BroadcastTransaction => None,
Self::SendOpenChannel => Some(VariableType::SentOpenChannel),
Self::RecvAcceptChannel => Some(VariableType::AcceptChannel),
Expand Down Expand Up @@ -709,6 +729,20 @@ impl Operation {
VariableType::Timestamp, // timestamp
VariableType::Bytes, // addresses
],

Self::BuildChannelUpdate => vec![
VariableType::PrivateKey, // node_sk
VariableType::ChainHash, // chain_hash
VariableType::ShortChannelId, // short_channel_id
VariableType::Timestamp, // timestamp
VariableType::U8, // message_flags
VariableType::U8, // channel_flags
VariableType::U16, // cltv_expiry_delta
VariableType::Amount, // htlc_minimum_msat
VariableType::ForwardingFee, // fee_base_msat
VariableType::ForwardingFee, // fee_proportional_millionths
VariableType::Amount, // htlc_maximum_msat
],
}
}

Expand Down Expand Up @@ -742,6 +776,7 @@ impl Operation {
| Self::BuildOpenChannel
| Self::BuildChannelAnnouncement
| Self::BuildNodeAnnouncement { .. }
| Self::BuildChannelUpdate
| Self::SendMessage
| Self::SendOpenChannel
| Self::MineBlocks(_)
Expand Down Expand Up @@ -785,7 +820,8 @@ impl Operation {
| Self::DerivePoint
| Self::ExtractAcceptChannel(_)
| Self::BuildOpenChannel
| Self::BuildNodeAnnouncement { .. } => false,
| Self::BuildNodeAnnouncement { .. }
| Self::BuildChannelUpdate => false,
}
}

Expand Down Expand Up @@ -818,6 +854,7 @@ impl Operation {
| Self::CreateFundingTransaction
| Self::BuildOpenChannel
| Self::BuildChannelAnnouncement
| Self::BuildChannelUpdate
| Self::SendMessage
| Self::SendOpenChannel
| Self::RecvAcceptChannel
Expand Down
84 changes: 84 additions & 0 deletions smite-ir/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,90 @@ fn display_build_node_announcement_program() {
}
}

#[test]
fn display_build_channel_update_program() {
let scid = ShortChannelId::new(538_532, 845, 1);
let instructions = vec![
Instruction {
operation: Operation::LoadPrivateKey(key(1)),
inputs: vec![],
},
Instruction {
operation: Operation::LoadChainHashFromContext,
inputs: vec![],
},
Instruction {
operation: Operation::LoadShortChannelId(scid.as_u64()),
inputs: vec![],
},
Instruction {
operation: Operation::LoadTimestamp(1_715_000_000),
inputs: vec![],
},
Instruction {
operation: Operation::LoadU8(0x01), // message_flags
inputs: vec![],
},
Instruction {
operation: Operation::LoadU8(0x00), // channel_flags
inputs: vec![],
},
Instruction {
operation: Operation::LoadU16(144), // cltv_expiry_delta
inputs: vec![],
},
Instruction {
operation: Operation::LoadAmount(1_000), // htlc_minimum_msat
inputs: vec![],
},
Instruction {
operation: Operation::LoadForwardingFee(1_000), // fee_base_msat
inputs: vec![],
},
Instruction {
operation: Operation::LoadForwardingFee(100), // fee_proportional_millionths
inputs: vec![],
},
Comment thread
morehouse marked this conversation as resolved.
Instruction {
operation: Operation::LoadAmount(99_000_000), // htlc_maximum_msat
inputs: vec![],
},
Instruction {
operation: Operation::BuildChannelUpdate,
inputs: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
Instruction {
operation: Operation::SendMessage,
inputs: vec![11],
},
];

let program = Program { instructions };
let text = program.to_string();
let lines: Vec<&str> = text.lines().collect();

let z31 = "00".repeat(31);
let expected: Vec<String> = vec![
format!("v0 = LoadPrivateKey(0x{z31}01)"),
"v1 = LoadChainHashFromContext()".into(),
"v2 = LoadShortChannelId(538532x845x1)".into(),
"v3 = LoadTimestamp(1715000000)".into(),
"v4 = LoadU8(1)".into(),
"v5 = LoadU8(0)".into(),
"v6 = LoadU16(144)".into(),
"v7 = LoadAmount(1000)".into(),
"v8 = LoadForwardingFee(1000)".into(),
"v9 = LoadForwardingFee(100)".into(),
"v10 = LoadAmount(99000000)".into(),
"v11 = BuildChannelUpdate(v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)".into(),
"SendMessage(v11)".into(),
];
assert_eq!(lines.len(), expected.len(), "line count mismatch");
for (i, (got, want)) in lines.iter().zip(expected.iter()).enumerate() {
assert_eq!(got, want, "line {i} mismatch");
}
}

#[test]
fn postcard_roundtrip() {
let program = Program {
Expand Down
153 changes: 151 additions & 2 deletions smite-scenarios/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use smite::bitcoin::{BitcoinCli, Utxo};
use smite::bolt::{
AcceptChannel, ChannelAnnouncement, ChannelId, Message, NodeAnnouncement, OpenChannel,
OpenChannelTlvs, Pong, ShortChannelId, msg_type,
AcceptChannel, ChannelAnnouncement, ChannelId, ChannelUpdate, Message, NodeAnnouncement,
OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, msg_type,
};
use smite::channel_tx::{FundingTransaction, build_funding_transaction};
use smite::noise::{ConnectionError, NoiseConnection};
Expand Down Expand Up @@ -215,6 +215,12 @@ pub fn execute(
Some(Variable::Message(encoded))
}

Operation::BuildChannelUpdate => {
let cu = build_channel_update(&variables, &instr.inputs);
let encoded = Message::ChannelUpdate(cu).encode();
Some(Variable::Message(encoded))
}

// -- Act operations --
Operation::SendMessage => {
let bytes = resolve_message(&variables, instr.inputs[0]);
Expand Down Expand Up @@ -306,6 +312,16 @@ fn resolve_feerate(variables: &[Option<Variable>], index: usize) -> u32 {
}
}

fn resolve_forwarding_fee(variables: &[Option<Variable>], index: usize) -> u32 {
match resolve(variables, index) {
Variable::ForwardingFee(v) => *v,
other => panic!(
"variable {index}: expected ForwardingFee, got {:?}",
other.var_type(),
),
}
}

fn resolve_timestamp(variables: &[Option<Variable>], index: usize) -> u32 {
match resolve(variables, index) {
Variable::Timestamp(v) => *v,
Expand Down Expand Up @@ -592,6 +608,41 @@ fn build_node_announcement(
na
}

/// Builds a signed `ChannelUpdate` from 11 input variables.
fn build_channel_update(variables: &[Option<Variable>], inputs: &[usize]) -> ChannelUpdate {
let sk_bytes = resolve_private_key(variables, inputs[0]);
let chain_hash = resolve_chain_hash(variables, inputs[1]);
let short_channel_id = resolve_short_channel_id(variables, inputs[2]);
let timestamp = resolve_timestamp(variables, inputs[3]);
let message_flags = resolve_u8(variables, inputs[4]);
let channel_flags = resolve_u8(variables, inputs[5]);
let cltv_expiry_delta = resolve_u16(variables, inputs[6]);
let htlc_minimum_msat = resolve_amount(variables, inputs[7]);
let fee_base_msat = resolve_forwarding_fee(variables, inputs[8]);
let fee_proportional_millionths = resolve_forwarding_fee(variables, inputs[9]);
let htlc_maximum_msat = resolve_amount(variables, inputs[10]);

let sk = SecretKey::from_slice(&sk_bytes).expect("valid private key");

let mut cu = ChannelUpdate {
signature: bitcoin::secp256k1::ecdsa::Signature::from_compact(&[0u8; 64])
.expect("zero bytes parse as a signature"),
chain_hash,
short_channel_id,
timestamp,
message_flags,
channel_flags,
cltv_expiry_delta,
htlc_minimum_msat,
fee_base_msat,
fee_proportional_millionths,
htlc_maximum_msat,
extra: Vec::new(),
};
cu.sign(&sk);
cu
}

/// Receives the next message of interest, auto-responding to pings and silently
/// skipping unknown odd-type messages.
#[allow(clippy::similar_names)] // ping and pong are canonical names
Expand Down Expand Up @@ -1159,6 +1210,104 @@ mod tests {
assert!(na.verify());
}

#[test]
fn execute_build_channel_update() {
let mut sk_bytes = [0u8; 32];
sk_bytes[31] = 0x42;
let scid = ShortChannelId::new(538_532, 845, 1);

let instrs = vec![
Instruction {
operation: Operation::LoadPrivateKey(sk_bytes),
inputs: vec![],
},
Instruction {
operation: Operation::LoadChainHashFromContext,
inputs: vec![],
},
Instruction {
operation: Operation::LoadShortChannelId(scid.as_u64()),
inputs: vec![],
},
Instruction {
operation: Operation::LoadTimestamp(1_715_000_000),
inputs: vec![],
},
Instruction {
operation: Operation::LoadU8(0x01), // message_flags: must_be_one
inputs: vec![],
},
Instruction {
operation: Operation::LoadU8(0x00), // channel_flags
inputs: vec![],
},
Instruction {
operation: Operation::LoadU16(144), // cltv_expiry_delta
inputs: vec![],
},
Instruction {
operation: Operation::LoadAmount(1_000), // htlc_minimum_msat
inputs: vec![],
},
Instruction {
operation: Operation::LoadForwardingFee(1_000), // fee_base_msat
inputs: vec![],
},
Instruction {
operation: Operation::LoadForwardingFee(100), // fee_proportional_millionths
inputs: vec![],
},
Instruction {
operation: Operation::LoadAmount(99_000_000), // htlc_maximum_msat
inputs: vec![],
},
Instruction {
operation: Operation::BuildChannelUpdate,
inputs: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
Instruction {
operation: Operation::SendMessage,
inputs: vec![11],
},
];

let program = Program {
instructions: instrs,
};
let mut conn = MockConnection::new();
execute(
&program,
&sample_context(),
&mut conn,
&mut MockBitcoinCli::default(),
std::time::Instant::now(),
)
.unwrap();

assert_eq!(conn.sent.len(), 1);
let cu = match Message::decode(&conn.sent[0]).expect("valid message") {
Message::ChannelUpdate(cu) => cu,
other => panic!("expected ChannelUpdate, got type {}", other.msg_type()),
};

assert_eq!(cu.chain_hash, sample_context().chain_hash);
assert_eq!(cu.short_channel_id, scid);
assert_eq!(cu.timestamp, 1_715_000_000);
assert_eq!(cu.message_flags, 0x01);
assert_eq!(cu.channel_flags, 0x00);
assert_eq!(cu.cltv_expiry_delta, 144);
assert_eq!(cu.htlc_minimum_msat, 1_000);
assert_eq!(cu.fee_base_msat, 1_000);
assert_eq!(cu.fee_proportional_millionths, 100);
assert_eq!(cu.htlc_maximum_msat, 99_000_000);
assert!(cu.extra.is_empty());

let secp = Secp256k1::new();
let expected_node_id =
PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&sk_bytes).unwrap());
assert!(cu.verify(&expected_node_id));
}

#[test]
fn execute_build_open_channel_with_tlvs() {
let mut instrs = open_channel_instructions();
Expand Down