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
5 changes: 5 additions & 0 deletions smite-ir/src/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod funding_created;
mod funding_flow;
mod node_announcement;
mod open_channel;
mod reorg_chain;

pub use channel_announcement::ChannelAnnouncementGenerator;
pub use channel_ready::ChannelReadyGenerator;
Expand All @@ -20,6 +21,7 @@ pub use funding_created::FundingCreatedGenerator;
pub use funding_flow::FundingFlowGenerator;
pub use node_announcement::NodeAnnouncementGenerator;
pub use open_channel::OpenChannelGenerator;
pub use reorg_chain::ReorgChainGenerator;

use rand::Rng;

Expand All @@ -42,6 +44,7 @@ pub enum AnyGenerator {
FundingCreated(FundingCreatedGenerator),
ChannelReady(ChannelReadyGenerator),
FundingFlow(FundingFlowGenerator),
ReorgChain(ReorgChainGenerator),
}

impl AnyGenerator {
Expand All @@ -54,6 +57,7 @@ impl AnyGenerator {
Self::FundingCreated(FundingCreatedGenerator),
Self::ChannelReady(ChannelReadyGenerator),
Self::FundingFlow(FundingFlowGenerator),
Self::ReorgChain(ReorgChainGenerator),
];
}

Expand All @@ -67,6 +71,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::ReorgChain(generator) => generator.generate(builder, rng),
}
}
}
26 changes: 26 additions & 0 deletions smite-ir/src/generators/reorg_chain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! Generator for chain reorganizations.

use rand::{Rng, RngExt};

use super::Generator;
use crate::Operation;
use crate::builder::ProgramBuilder;

/// Generates a shallow chain reorganization.
///
/// Emits instructions to:
/// 1. Mine blocks to confirm any broadcast transaction
/// 2. Reorg the chain
#[derive(Clone, Copy)]
pub struct ReorgChainGenerator;

impl Generator for ReorgChainGenerator {
fn generate(&self, builder: &mut ProgramBuilder, rng: &mut impl Rng) {
// Mine blocks to confirm any broadcast transaction.
builder.append(Operation::MineBlocks(rng.random_range(1..=16)), &[]);

// One or two block reorgs occur naturally on mainnet and are therefore
// the shallow reorgs a Lightning node is expected to handle.
builder.append(Operation::ReorgChain(rng.random_range(1..=2)), &[]);
}
}
7 changes: 7 additions & 0 deletions smite-ir/src/mutators/operation_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool {
*v = rng.random_range(1..=16);
true
}
Operation::ReorgChain(v) => {
// One or two block reorgs occur naturally on mainnet and are
// therefore the shallow reorgs a Lightning node is expected to
// handle.
*v = rng.random_range(1..=2);
true
}
Operation::ExtractAcceptChannel(field) => mutate_extract_field(field, rng),
Operation::BuildNodeAnnouncement { rgb_color, alias } => {
// Randomly mutate rgb_color or alias bytes in place; never change
Expand Down
12 changes: 10 additions & 2 deletions smite-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ pub enum Operation {
RecvChannelReady,
/// Mines the given number of blocks on the Bitcoin network.
MineBlocks(u8),
/// Reorganizes the given number of blocks at the tip of the Bitcoin chain.
ReorgChain(u8),
/// Sign wallet inputs of the transaction and broadcast it via `bitcoin-cli`.
/// Input: `FundingTransaction`.
BroadcastTransaction,
Expand Down Expand Up @@ -689,6 +691,7 @@ impl fmt::Display for Operation {
Self::LoadTargetPubkeyFromContext => write!(f, "LoadTargetPubkeyFromContext()"),
Self::LoadChainHashFromContext => write!(f, "LoadChainHashFromContext()"),
Self::MineBlocks(v) => write!(f, "MineBlocks({v})"),
Self::ReorgChain(v) => write!(f, "ReorgChain({v})"),
// Operations with inputs: parens added by Program::Display.
Self::DerivePoint => write!(f, "DerivePoint"),
Self::ExtractAcceptChannel(field) => write!(f, "Extract{field}"),
Expand Down Expand Up @@ -752,6 +755,7 @@ impl Operation {
| Self::SendChannelReady { .. }
| Self::RecvChannelReady
| Self::MineBlocks(_)
| Self::ReorgChain(_)
| Self::BroadcastTransaction => None,
Self::SendOpenChannel => Some(VariableType::SentOpenChannel),
Self::SendFundingCreated => Some(VariableType::SentFundingCreated),
Expand Down Expand Up @@ -782,7 +786,8 @@ impl Operation {
| Self::LoadTargetPubkeyFromContext
| Self::LoadChainHashFromContext
| Self::RecvChannelReady
| Self::MineBlocks(_) => vec![],
| Self::MineBlocks(_)
| Self::ReorgChain(_) => vec![],

Self::DerivePoint => vec![VariableType::PrivateKey],
Self::ExtractAcceptChannel(_) => vec![VariableType::AcceptChannel],
Expand Down Expand Up @@ -921,6 +926,7 @@ impl Operation {
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::MineBlocks(_)
| Self::ReorgChain(_)
| Self::BroadcastTransaction
| Self::LookupShortChannelId => vec![],

Expand All @@ -945,6 +951,7 @@ impl Operation {
| Self::RecvFundingSigned
| Self::RecvChannelReady
| Self::MineBlocks(_)
| Self::ReorgChain(_)
| Self::CreateFundingTransaction
| Self::BroadcastTransaction
| Self::LookupShortChannelId => true,
Expand Down Expand Up @@ -996,7 +1003,8 @@ impl Operation {
| Self::ExtractAcceptChannel(_)
| Self::BuildNodeAnnouncement { .. }
| Self::SendChannelReady { .. }
| Self::MineBlocks(_) => true,
| Self::MineBlocks(_)
| Self::ReorgChain(_) => true,

Self::LoadTargetPubkeyFromContext
| Self::LoadChainHashFromContext
Expand Down
122 changes: 114 additions & 8 deletions smite-ir/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::*;
use generators::{
AnyGenerator, ChannelAnnouncementGenerator, ChannelReadyGenerator, ChannelUpdateGenerator,
FundingCreatedGenerator, FundingFlowGenerator, NodeAnnouncementGenerator, OpenChannelGenerator,
ReorgChainGenerator,
};
use minimizers::{CommonSubexpressionEliminator, DeadCodeEliminator, Minimizer};
use mutators::{
Expand Down Expand Up @@ -670,16 +671,22 @@ fn mine_blocks_operation() {
}

#[test]
fn displays_mine_blocks_program() {
fn displays_mine_and_reorg_blocks_program() {
let program = Program {
instructions: vec![Instruction {
operation: Operation::MineBlocks(6),
inputs: vec![],
}],
instructions: vec![
Instruction {
operation: Operation::MineBlocks(6),
inputs: vec![],
},
Instruction {
operation: Operation::ReorgChain(2),
inputs: vec![],
},
],
};
let text = program.to_string();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, vec!["MineBlocks(6)"]);
assert_eq!(lines, vec!["MineBlocks(6)", "ReorgChain(2)"]);
}

#[test]
Expand Down Expand Up @@ -886,7 +893,8 @@ fn any_generator_all_is_complete() {
| AnyGenerator::OpenChannel(_)
| AnyGenerator::FundingCreated(_)
| AnyGenerator::ChannelReady(_)
| AnyGenerator::FundingFlow(_) => 7,
| AnyGenerator::FundingFlow(_)
| AnyGenerator::ReorgChain(_) => 8,
}
};
assert_eq!(AnyGenerator::ALL.len(), variant_count(AnyGenerator::ALL[0]));
Expand Down Expand Up @@ -1351,6 +1359,65 @@ fn generated_funding_flow_program_structure() {
);
}

fn generate_reorg_chain_program(seed: u64) -> Program {
let mut rng = SmallRng::seed_from_u64(seed);
let mut builder = ProgramBuilder::new();
ReorgChainGenerator.generate(&mut builder, &mut rng);
builder.build()
}

// If ReorgChainGenerator completes without panicking, every instruction has
// correct input types (enforced by ProgramBuilder::append).
#[test]
fn generated_reorg_chain_program_is_type_correct() {
for seed in 0..100 {
generate_reorg_chain_program(seed);
}
}

#[test]
fn generated_reorg_chain_program_structure() {
let program = generate_reorg_chain_program(0);
let ops: Vec<_> = program.instructions.iter().map(|i| &i.operation).collect();

// Must be MineBlocks followed by ReorgChain, and nothing else.
assert_eq!(
ops.len(),
2,
"expected exactly two instructions, got {ops:?}"
);
assert!(
matches!(ops[0], Operation::MineBlocks(_)),
"first instruction should be MineBlocks",
);
assert!(
matches!(ops[1], Operation::ReorgChain(_)),
"last instruction should be ReorgChain",
);
}

// The reorg depth must stay shallow: deeper reorgs do not occur naturally on
// mainnet and would only cost execution time.
#[test]
fn generated_reorg_chain_program_respects_bounds() {
for seed in 0..100 {
let program = generate_reorg_chain_program(seed);
for instruction in &program.instructions {
match instruction.operation {
Operation::MineBlocks(blocks) => assert!(
(1..=16).contains(&blocks),
"seed {seed}: MineBlocks({blocks}) out of range",
),
Operation::ReorgChain(depth) => assert!(
(1..=2).contains(&depth),
"seed {seed}: ReorgChain({depth}) out of range",
),
ref op => panic!("seed {seed}: unexpected operation {op}"),
}
}
}
}

fn generate_channel_announcement_program(seed: u64) -> Program {
let mut rng = SmallRng::seed_from_u64(seed);
let mut builder = ProgramBuilder::new();
Expand Down Expand Up @@ -1498,6 +1565,14 @@ fn generated_funding_flow_program_postcard_roundtrip() {
assert_eq!(program, decoded);
}

#[test]
fn generated_reorg_chain_program_postcard_roundtrip() {
let program = generate_reorg_chain_program(42);
let bytes = postcard::to_allocvec(&program).expect("postcard serialization");
let decoded: Program = postcard::from_bytes(&bytes).expect("postcard deserialization");
assert_eq!(program, decoded);
}

#[test]
fn generated_channel_announcement_program_postcard_roundtrip() {
let program = generate_channel_announcement_program(42);
Expand Down Expand Up @@ -1752,7 +1827,7 @@ fn param_mutator_changes_values() {
fn param_mutator_changes_mined_num_blocks() {
let original = Program {
instructions: vec![Instruction {
operation: Operation::MineBlocks(42),
operation: Operation::MineBlocks(8),
inputs: vec![],
}],
};
Expand All @@ -1779,6 +1854,37 @@ fn param_mutator_changes_mined_num_blocks() {
);
}

#[test]
fn param_mutator_changes_reorg_num_blocks() {
let original = Program {
instructions: vec![Instruction {
operation: Operation::ReorgChain(2),
inputs: vec![],
}],
};
let mut program = original.clone();
let mutator = OperationParamMutator;
let mut rng = SmallRng::seed_from_u64(0);

let mut diff_count = 0;
for _ in 0..100 {
mutator.mutate(&mut program, &mut rng);
// Make sure that ReorgChain contains a value within the clamped range of
// blocks to be reorged.
let Operation::ReorgChain(depth) = program.instructions[0].operation else {
panic!("OperationParamMutator changed the operation type");
};
assert!((1..=2).contains(&depth));
if program != original {
diff_count += 1;
}
}
assert!(
diff_count > 35,
"OperationParamMutator has an unexpected bias"
);
}

#[test]
fn param_mutator_changes_short_channel_id() {
let original = Program {
Expand Down
Loading