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
61 changes: 57 additions & 4 deletions smite-ir/src/generators/channel_announcement.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
//! Generator for `channel_announcement` gossip message.

use rand::Rng;
use rand::{Rng, RngExt};

use super::Generator;
use crate::builder::ProgramBuilder;
use crate::{Operation, VariableType};

/// Generates an unsolicited `channel_announcement` send.
/// Generates an unsolicited `channel_announcement` send backed by a real
/// funding output.
///
/// Emits instructions to:
/// 1. Create, broadcast, and confirm a 2-of-2 P2WSH funding transaction
/// 2. Look up the `short_channel_id` of the confirmed funding output
/// 3. Build and send `channel_announcement`
///
/// The announced bitcoin keys are the ones committed to by the funding
/// output's witness script, so the message can pass the on-chain UTXO
/// validation that lightning implementations perform.
#[derive(Clone, Copy)]
pub struct ChannelAnnouncementGenerator;

impl ChannelAnnouncementGenerator {
/// BOLT 2 caps `funding_satoshis` at 2^24-1 for non-wumbo channels, and we
/// only have so much wallet balance to spend.
pub const MAX_FUNDING_SATOSHIS: u64 = 16_777_215;

/// Bounded so that fees don't exhaust the wallet.
pub const MAX_FEERATE_PER_KW: u32 = 10_000;
}

impl Generator for ChannelAnnouncementGenerator {
fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) {
let features = builder.pick_variable(VariableType::Features, rng);
let chain_hash = builder.pick_variable(VariableType::ChainHash, rng);
let scid = builder.pick_variable(VariableType::ShortChannelId, rng);

// Use fresh private keys since channel_announcement validation
// generally requires distinct keys.
Expand All @@ -23,12 +41,47 @@ impl Generator for ChannelAnnouncementGenerator {
let bitcoin_sk_1 = builder.generate_fresh(VariableType::PrivateKey, rng);
let bitcoin_sk_2 = builder.generate_fresh(VariableType::PrivateKey, rng);

// The funding output must pay to the same bitcoin keys the message
// announces, so derive the funding pubkeys instead of picking them.
let funding_pubkey_1 = builder.append(Operation::DerivePoint, &[bitcoin_sk_1]);
let funding_pubkey_2 = builder.append(Operation::DerivePoint, &[bitcoin_sk_2]);

// Load the funding amount and feerate rather than picking them, since a
// full-range random amount exceeds Bitcoin's maximum supply and fails
// coin selection, aborting the program before the announcement is sent.
let funding_satoshis = builder.append(
Operation::LoadAmount(rng.random_range(0..=Self::MAX_FUNDING_SATOSHIS)),
&[],
);
let feerate_per_kw = builder.append(
Operation::LoadFeeratePerKw(rng.random_range(0..=Self::MAX_FEERATE_PER_KW)),
&[],
);

// Create the 2-of-2 P2WSH funding transaction and confirm it.
let funding_transaction = builder.append(
Operation::CreateFundingTransaction,
&[
funding_pubkey_1,
funding_pubkey_2,
funding_satoshis,
feerate_per_kw,
],
);
builder.append(Operation::BroadcastTransaction, &[funding_transaction]);
builder.append(Operation::MineBlocks(rng.random_range(1..=16)), &[]);

// Derive the short_channel_id from the confirmed funding output.
let short_channel_id =
builder.append(Operation::LookupShortChannelId, &[funding_transaction]);

// Build and send channel_announcement.
let msg = builder.append(
Operation::BuildChannelAnnouncement,
&[
features,
chain_hash,
scid,
short_channel_id,
node_sk_1,
node_sk_2,
bitcoin_sk_1,
Expand Down
126 changes: 112 additions & 14 deletions smite-ir/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ fn assert_well_formed(program: &Program) {
}
}

/// Returns the index of the first instruction in `$program` whose operation
/// matches `$pattern`.
///
/// # Panics
///
/// Panics if `$program` contains no matching instruction.
macro_rules! find_operation {
($program:expr, $pattern:pat) => {
$program
.instructions
.iter()
.position(|i| matches!(i.operation, $pattern))
.unwrap_or_else(|| panic!("expected a {}", stringify!($pattern)))
};
}

#[test]
#[allow(clippy::too_many_lines)]
fn display_open_channel_program() {
Expand Down Expand Up @@ -1321,18 +1337,12 @@ fn generated_funding_flow_program_structure() {
);

// Key operations must appear in protocol order.
let position = |pred: fn(&Operation) -> bool| {
ops.iter()
.position(|op| pred(op))
.expect("operation present")
};

let recv_accept_channel = position(|op| matches!(op, Operation::RecvAcceptChannel));
let send_funding_created = position(|op| matches!(op, Operation::SendFundingCreated));
let recv_funding_signed = position(|op| matches!(op, Operation::RecvFundingSigned));
let broadcast_transaction = position(|op| matches!(op, Operation::BroadcastTransaction));
let send_channel_ready = position(|op| matches!(op, Operation::SendChannelReady { .. }));
let recv_channel_ready = position(|op| matches!(op, Operation::RecvChannelReady));
let recv_accept_channel = find_operation!(program, Operation::RecvAcceptChannel);
let send_funding_created = find_operation!(program, Operation::SendFundingCreated);
let recv_funding_signed = find_operation!(program, Operation::RecvFundingSigned);
let broadcast_transaction = find_operation!(program, Operation::BroadcastTransaction);
let send_channel_ready = find_operation!(program, Operation::SendChannelReady { .. });
let recv_channel_ready = find_operation!(program, Operation::RecvChannelReady);

assert!(
recv_accept_channel < send_funding_created,
Expand Down Expand Up @@ -1396,6 +1406,94 @@ fn generated_channel_announcement_program_structure() {
build_count, 1,
"expected exactly one BuildChannelAnnouncement"
);

// The funding transaction must be created, broadcast, and confirmed before
// its short_channel_id is looked up.
let create = find_operation!(program, Operation::CreateFundingTransaction);
let broadcast = find_operation!(program, Operation::BroadcastTransaction);
let mine = find_operation!(program, Operation::MineBlocks(_));
let lookup = find_operation!(program, Operation::LookupShortChannelId);
assert!(
create < broadcast && broadcast < mine && mine < lookup,
"expected Create < Broadcast < Mine < Lookup, got {create} {broadcast} {mine} {lookup}",
);
}

// The bitcoin keys the message announces must be the ones committed to by
// the funding output's witness script, and the announced scid must come from
// that same funding transaction. Otherwise the announcement cannot pass
// on-chain validation.
#[test]
fn generated_channel_announcement_commits_to_its_funding_output() {
let program = generate_channel_announcement_program(0);

let create_idx = find_operation!(program, Operation::CreateFundingTransaction);
let lookup_idx = find_operation!(program, Operation::LookupShortChannelId);
let build_idx = find_operation!(program, Operation::BuildChannelAnnouncement);

let create = &program.instructions[create_idx];
let lookup = &program.instructions[lookup_idx];
let build = &program.instructions[build_idx];

// The scid is looked up from the funding transaction just created, and
// that scid is what the announcement carries. LookupShortChannelId input 0
// is the funding transaction; BuildChannelAnnouncement input 2 is the
// announced short_channel_id.
assert_eq!(
lookup.inputs[0], create_idx,
"LookupShortChannelId should read the funding transaction just created",
);
assert_eq!(
build.inputs[2], lookup_idx,
"the announced short_channel_id should be the one looked up from the funding transaction",
);

// Each announced bitcoin key is the private key behind the corresponding
// funding pubkey. CreateFundingTransaction inputs 0 and 1 are the funding
// pubkeys; BuildChannelAnnouncement inputs 5 and 6 are the announced
// bitcoin_key_1 and bitcoin_key_2.
for (funding_pos, announced_pos) in [(0, 5), (1, 6)] {
let derive = &program.instructions[create.inputs[funding_pos]];
assert!(
matches!(derive.operation, Operation::DerivePoint),
"funding pubkey {funding_pos} should be a DerivePoint",
);
assert_eq!(
derive.inputs[0],
build.inputs[announced_pos],
"bitcoin_key_{} must be the private key behind funding pubkey {funding_pos}",
funding_pos + 1,
);
}
}

// Coin selection fails outright for amounts above Bitcoin's maximum supply, so
// the generator must emit plausible funding values rather than full-range
// randoms. Without this the program exits before sending the announcement.
#[test]
fn generated_channel_announcement_uses_plausible_funding_values() {
for seed in 0..100 {
let program = generate_channel_announcement_program(seed);
let create_idx = find_operation!(program, Operation::CreateFundingTransaction);
let create = &program.instructions[create_idx];

// CreateFundingTransaction input 2 is the funding amount and input 3
// is the feerate.
match &program.instructions[create.inputs[2]].operation {
Operation::LoadAmount(sats) => assert!(
*sats <= ChannelAnnouncementGenerator::MAX_FUNDING_SATOSHIS,
"seed {seed}: implausible funding amount {sats}",
),
op => panic!("seed {seed}: expected LoadAmount, got {op}"),
}
match &program.instructions[create.inputs[3]].operation {
Operation::LoadFeeratePerKw(feerate) => assert!(
*feerate <= ChannelAnnouncementGenerator::MAX_FEERATE_PER_KW,
"seed {seed}: implausible feerate {feerate}",
),
op => panic!("seed {seed}: expected LoadFeeratePerKw, got {op}"),
}
}
}

fn generate_node_announcement_program(seed: u64) -> Program {
Expand Down Expand Up @@ -2291,8 +2389,8 @@ fn instr_reorder_returns_false_on_empty() {

#[test]
fn instr_reorder_returns_false_on_single_or_no_act() {
// ChannelAnnouncementGenerator produces a single Act instruction: SendMessage.
let mut program = generate_channel_announcement_program(0);
// NodeAnnouncementGenerator produces a single Act instruction: SendMessage.
let mut program = generate_node_announcement_program(0);
let mutator = InstructionReorderMutator;
let mut rng = SmallRng::seed_from_u64(0);

Expand Down