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
35 changes: 31 additions & 4 deletions smite-ir-mutator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
//! bypasses our custom mutators. This was an AFL++ bug fixed upstream in
//! commit eddb2701b022351fb34b696ccf923bb856e9d953.
//!
//! Optionally:
//! - `SMITE_IR_GENERATORS=v1|v2|all` -- which generators to draw from. BOLT 2
//! makes the two channel establishment flows mutually exclusive on one
//! connection, so a campaign against an `ir` scenario wants `v1` and one
//! against `ir_v2` wants `v2`; the other flow's programs would only ever be
//! rejected. Defaults to `all`, which draws from both.
//!
//! # Logging
//!
//! Logging is opt-in: [`afl_custom_init`] installs a logger only when
Expand Down Expand Up @@ -60,6 +67,24 @@ struct MutatorState {
/// Sequence of actions taken in the last [`afl_custom_fuzz`] call, used by
/// [`afl_custom_describe`] to name queue entries.
last_sequence: Vec<&'static str>,
/// Generators this campaign draws from, selected by `SMITE_IR_GENERATORS`.
generators: &'static [AnyGenerator],
}

/// Reads `SMITE_IR_GENERATORS` and returns the generator set it names,
/// defaulting to all of them.
fn generators_from_env() -> &'static [AnyGenerator] {
match std::env::var("SMITE_IR_GENERATORS").as_deref() {
Ok("v1") => AnyGenerator::V1,
Ok("v2") => AnyGenerator::V2,
Ok("all") | Err(_) => AnyGenerator::ALL,
Ok(other) => {
eprintln!(
"[smite-ir-mutator] WARNING: unknown SMITE_IR_GENERATORS={other:?}, using all",
);
AnyGenerator::ALL
}
}
}

impl MutatorState {
Expand All @@ -69,17 +94,18 @@ impl MutatorState {
out_buf: Vec::new(),
description: Vec::new(),
last_sequence: vec!["init"],
generators: generators_from_env(),
}
}

/// Generates a fresh program from scratch by randomly delegating to one of
/// the registered generators.
fn generate_fresh(&mut self) -> Program {
let mut builder = ProgramBuilder::new();
AnyGenerator::ALL
self.generators
.iter()
.choose(&mut self.rng)
.expect("AnyGenerator::ALL is non-empty")
.expect("the generator set is non-empty")
.generate(&mut builder, &mut self.rng);
self.last_sequence.clear();
self.last_sequence.push("fresh");
Expand Down Expand Up @@ -114,10 +140,11 @@ impl MutatorState {
"instr-reorder"
}
4 => {
let generator = *AnyGenerator::ALL
let generator = *self
.generators
.iter()
.choose(&mut self.rng)
.expect("AnyGenerator::ALL is non-empty");
.expect("the generator set is non-empty");
let mutator = GeneratorInsertionMutator::new(generator);
mutator.mutate(program, &mut self.rng);
"gen-insert"
Expand Down
15 changes: 15 additions & 0 deletions smite-ir/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,27 @@ impl ProgramBuilder {
VariableType::AcceptChannel => {
panic!("cannot generate fresh AcceptChannel: requires protocol interaction")
}
VariableType::OpenChannel2Message => {
panic!("cannot generate fresh OpenChannel2Message: requires composed inputs")
}
VariableType::AcceptChannel2 => {
panic!("cannot generate fresh AcceptChannel2: requires protocol interaction")
}
VariableType::FundingTransaction => {
panic!("cannot generate fresh FundingTransaction: requires composed inputs")
}
VariableType::SentOpenChannel => {
panic!("cannot generate fresh SentOpenChannel: affine type")
}
VariableType::SentOpenChannel2 => {
panic!("cannot generate fresh SentOpenChannel2: affine type")
}
VariableType::SentInteractiveTx => {
panic!("cannot generate fresh SentInteractiveTx: affine type")
}
VariableType::SentCommitmentSigned => {
panic!("cannot generate fresh SentCommitmentSigned: affine type")
}
VariableType::SentFundingCreated => {
panic!("cannot generate fresh SentFundingCreated: affine type")
}
Expand Down
32 changes: 32 additions & 0 deletions smite-ir/src/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
mod channel_announcement;
mod channel_ready;
mod channel_update;
mod dual_funding_flow;
mod funding_created;
mod funding_flow;
mod node_announcement;
Expand All @@ -16,6 +17,7 @@ mod open_channel;
pub use channel_announcement::ChannelAnnouncementGenerator;
pub use channel_ready::ChannelReadyGenerator;
pub use channel_update::ChannelUpdateGenerator;
pub use dual_funding_flow::DualFundingFlowGenerator;
pub use funding_created::FundingCreatedGenerator;
pub use funding_flow::FundingFlowGenerator;
pub use node_announcement::NodeAnnouncementGenerator;
Expand All @@ -42,9 +44,37 @@ pub enum AnyGenerator {
FundingCreated(FundingCreatedGenerator),
ChannelReady(ChannelReadyGenerator),
FundingFlow(FundingFlowGenerator),
DualFundingFlow(DualFundingFlowGenerator),
}

impl AnyGenerator {
/// Generators for the v1 (single-funded) channel establishment flow, plus
/// the gossip generators, which are flow-independent.
///
/// BOLT 2 makes the two establishment flows mutually exclusive on one
/// connection, so a campaign negotiating `option_dual_fund` can only ever
/// have the v1 generators rejected, and vice versa. Splitting them lets a
/// campaign spend its executions on programs its target can act on.
pub const V1: &[Self] = &[
Self::ChannelAnnouncement(ChannelAnnouncementGenerator),
Self::ChannelUpdate(ChannelUpdateGenerator),
Self::NodeAnnouncement(NodeAnnouncementGenerator),
Self::OpenChannel(OpenChannelGenerator),
Self::FundingCreated(FundingCreatedGenerator),
Self::ChannelReady(ChannelReadyGenerator),
Self::FundingFlow(FundingFlowGenerator),
];

/// Generators for the v2 (dual-funded) channel establishment flow, plus the
/// gossip generators. See [`Self::V1`].
pub const V2: &[Self] = &[
Self::ChannelAnnouncement(ChannelAnnouncementGenerator),
Self::ChannelUpdate(ChannelUpdateGenerator),
Self::NodeAnnouncement(NodeAnnouncementGenerator),
Self::ChannelReady(ChannelReadyGenerator),
Self::DualFundingFlow(DualFundingFlowGenerator),
];

/// All variants. Keep in sync with the enum definition.
pub const ALL: &[Self] = &[
Self::ChannelAnnouncement(ChannelAnnouncementGenerator),
Expand All @@ -54,6 +84,7 @@ impl AnyGenerator {
Self::FundingCreated(FundingCreatedGenerator),
Self::ChannelReady(ChannelReadyGenerator),
Self::FundingFlow(FundingFlowGenerator),
Self::DualFundingFlow(DualFundingFlowGenerator),
];
}

Expand All @@ -67,6 +98,7 @@ impl Generator for AnyGenerator {
Self::FundingCreated(generator) => generator.generate(builder, rng),
Self::ChannelReady(generator) => generator.generate(builder, rng),
Self::FundingFlow(generator) => generator.generate(builder, rng),
Self::DualFundingFlow(generator) => generator.generate(builder, rng),
}
}
}
222 changes: 222 additions & 0 deletions smite-ir/src/generators/dual_funding_flow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
//! Generator for the complete channel establishment v2 (dual-funded) flow.

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

use super::Generator;
use crate::builder::ProgramBuilder;
use crate::operation::{AcceptChannel2Field, ShutdownScriptVariant, TxOutputRole};
use crate::{Operation, VariableType};
use smite::bolt::ChannelTypeVariant;

/// `serial_id` of the funding output we contribute. BOLT 2 requires the
/// initiator to use even ids; picking these from a high range keeps them clear
/// of the ones assigned to inputs.
const FUNDING_OUTPUT_SERIAL_ID: u64 = 2000;

/// `serial_id` of our change output.
const CHANGE_OUTPUT_SERIAL_ID: u64 = 2002;

/// `nSequence` for the inputs we contribute. BOLT 2 caps it at `0xfffffffd` so
/// every input signals replaceability, and recommends one shared value across
/// implementations to avoid fingerprinting.
const SEQUENCE: u32 = 0xffff_fffd;

/// Channel types most likely to be accepted, so the flow reaches its later
/// steps often enough to cover them. `LoadChannelType` is mutable, so the
/// mutator still reaches the rest.
const LIKELY_CHANNEL_TYPES: &[ChannelTypeVariant] = &[
ChannelTypeVariant::Anchors,
ChannelTypeVariant::StaticRemoteKey,
];

/// Generates the complete channel establishment v2 flow.
///
/// Emits instructions to:
/// 1. Build and send `open_channel2`, then receive `accept_channel2`
/// 2. Contribute inputs, the funding output and a change output through
/// interactive transaction construction, concluding with `tx_complete`
/// 3. Exchange `commitment_signed`, then `tx_signatures`
/// 4. Broadcast and confirm the funding transaction
/// 5. Complete the `channel_ready` exchange
#[derive(Clone, Copy)]
pub struct DualFundingFlowGenerator;

impl Generator for DualFundingFlowGenerator {
// One linear protocol script, from open_channel2 through channel_ready.
// Splitting it would scatter a sequence that reads best in wire order.
#[allow(clippy::too_many_lines)]
fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) {
// Keys are generated fresh to ensure they're distinct.
let funding_privkey = builder.generate_fresh(VariableType::PrivateKey, rng);
let funding_pubkey = builder.append(Operation::DerivePoint, &[funding_privkey]);
let revocation_basepoint = builder.generate_fresh(VariableType::Point, rng);
let payment_basepoint = builder.generate_fresh(VariableType::Point, rng);
let delayed_payment_basepoint = builder.generate_fresh(VariableType::Point, rng);
let htlc_basepoint = builder.generate_fresh(VariableType::Point, rng);
let first_per_commitment_point = builder.generate_fresh(VariableType::Point, rng);
let second_per_commitment_point = builder.generate_fresh(VariableType::Point, rng);

// BOLT 2 derives the v2 temporary_channel_id from our revocation
// basepoint with a zeroed one standing in for the peer.
let temporary_channel_id = builder.append(
Operation::DeriveTemporaryChannelIdV2,
&[revocation_basepoint],
);

let chain_hash = builder.pick_variable(VariableType::ChainHash, rng);
let funding_satoshis = builder.append(
Operation::LoadAmount(rng.random_range(100_000..=1_000_000)),
&[],
);
let funding_feerate_perkw = builder.append(
Operation::LoadFeeratePerKw(rng.random_range(253..=2_000)),
&[],
);
let commitment_feerate_perkw = builder.append(
Operation::LoadFeeratePerKw(rng.random_range(253..=5_000)),
&[],
);
let dust_limit_satoshis = builder.append(Operation::LoadAmount(546), &[]);
let max_htlc_value_in_flight_msat = builder.append(Operation::LoadAmount(100_000_000), &[]);
let htlc_minimum_msat = builder.append(Operation::LoadAmount(1), &[]);
let to_self_delay = builder.append(Operation::LoadU16(144), &[]);
let max_accepted_htlcs = builder.append(Operation::LoadU16(483), &[]);
let locktime = builder.append(Operation::LoadBlockHeight(0), &[]);
let channel_flags = builder.append(Operation::LoadU8(u8::from(rng.random::<bool>())), &[]);
let upfront_shutdown_script = builder.append(
Operation::LoadShutdownScript(ShutdownScriptVariant::Empty),
&[],
);
let channel_type_variant = if rng.random_range(0..4) == 0 {
*ChannelTypeVariant::ALL
.choose(rng)
.expect("ChannelTypeVariant::ALL is non-empty")
} else {
*LIKELY_CHANNEL_TYPES
.choose(rng)
.expect("LIKELY_CHANNEL_TYPES is non-empty")
};
let channel_type = builder.append(Operation::LoadChannelType(channel_type_variant), &[]);

// Build and send open_channel2.
let open_channel2_msg = builder.append(
Operation::BuildOpenChannel2 {
require_confirmed_inputs: rng.random_range(0..8) == 0,
},
&[
chain_hash,
temporary_channel_id,
funding_feerate_perkw,
commitment_feerate_perkw,
funding_satoshis,
dust_limit_satoshis,
max_htlc_value_in_flight_msat,
htlc_minimum_msat,
to_self_delay,
max_accepted_htlcs,
locktime,
funding_pubkey,
revocation_basepoint,
payment_basepoint,
delayed_payment_basepoint,
htlc_basepoint,
first_per_commitment_point,
second_per_commitment_point,
channel_flags,
upfront_shutdown_script,
channel_type,
],
);
let sent_open_channel2 = builder.append(Operation::SendOpenChannel2, &[open_channel2_msg]);

// Receive accept_channel2, which reveals the peer's revocation
// basepoint and so the channel_id every later message carries.
let accept_channel2 = builder.append(Operation::RecvAcceptChannel2, &[sent_open_channel2]);
let peer_revocation_basepoint = builder.append(
Operation::ExtractAcceptChannel2(AcceptChannel2Field::RevocationBasepoint),
&[accept_channel2],
);
let channel_id = builder.append(
Operation::DeriveChannelIdV2,
&[revocation_basepoint, peer_revocation_basepoint],
);

// Interactive transaction construction. The protocol is turn-based, so
// every contribution we send is followed by the peer's reply.
for i in 0..rng.random_range(1u8..=3) {
let sent = builder.append(
Operation::SendTxAddInput {
// Even ids, as BOLT 2 requires of the initiator.
serial_id: 2 * (u64::from(i) + 1),
utxo_index: i,
sequence: SEQUENCE,
},
&[channel_id],
);
builder.append(Operation::RecvInteractiveTx, &[sent]);
}

// The opener must contribute the funding output, and pays its fees.
for (serial_id, role) in [
(FUNDING_OUTPUT_SERIAL_ID, TxOutputRole::Funding),
(CHANGE_OUTPUT_SERIAL_ID, TxOutputRole::Change),
] {
let sent = builder.append(
Operation::SendTxAddOutput { serial_id, role },
// The value and script are derived from the negotiation for
// both roles here; they matter only once a mutator switches
// the role to `Explicit`.
&[channel_id, funding_satoshis, upfront_shutdown_script],
);
builder.append(Operation::RecvInteractiveTx, &[sent]);
}

// The exchange ends once both sides have sent `tx_complete` back to
// back. If the peer already sent one, ours ends it and nothing more
// arrives. If the peer contributed instead, it still has to answer
// ours with its own `tx_complete`. The executor tells the two cases
// apart at runtime, so this receive reads only when a reply is owed.
let sent_tx_complete = builder.append(Operation::SendTxComplete, &[channel_id]);
builder.append(Operation::RecvInteractiveTx, &[sent_tx_complete]);

// Exchange commitment signatures over the negotiated transaction.
let funding_transaction =
builder.append(Operation::BuildFundingTransactionV2, &[channel_id]);
let sent_commitment_signed = builder.append(
Operation::SendCommitmentSigned,
&[funding_transaction, funding_privkey, channel_id],
);
let funded_channel_id =
builder.append(Operation::RecvCommitmentSigned, &[sent_commitment_signed]);

// We contribute every input, so BOLT 2 has the peer send its
// tx_signatures first.
builder.append(Operation::RecvTxSignatures, &[channel_id]);
builder.append(
Operation::SendTxSignatures,
&[channel_id, funding_transaction],
);
builder.append(Operation::RecvTxSignatures, &[channel_id]);

builder.append(Operation::BroadcastTransaction, &[funding_transaction]);
builder.append(Operation::MineBlocks(rng.random_range(1..=16)), &[]);

// Reuse the second_per_commitment_point already committed to in
// open_channel2: implementations may cross-check the two, and feeding
// an unrelated point would fail channel_ready for a reason that has
// nothing to do with the flow under test.
let short_channel_id = builder.generate_fresh(VariableType::ShortChannelId, rng);
builder.append(
Operation::SendChannelReady {
include_alias: rng.random(),
},
&[
funded_channel_id,
second_per_commitment_point,
short_channel_id,
],
);
builder.append(Operation::RecvChannelReady, &[]);
}
}
Loading
Loading