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
10 changes: 10 additions & 0 deletions smite-ir/src/mutators/operation_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool {
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
// their lengths (array types prevent it).
if rng.random() {
mutate_fixed_bytes(rgb_color, rng);
} else {
mutate_fixed_bytes(alias, rng);
}
true
}

// Non-mutable variants. Reaching here means `is_param_mutable` and this
// match have drifted out of sync.
Expand Down
39 changes: 36 additions & 3 deletions smite-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ pub enum Operation {
/// 18: `upfront_shutdown_script` (`Bytes`, empty = omit TLV)
/// 19: `channel_type` (`Features`, empty = omit TLV)
BuildOpenChannel,
/// Build a `node_announcement` message (BOLT 7, type 257).
///
/// `rgb_color` and `alias` are op-level params (not variable inputs) so the
/// mutator can flip bits inside them but cannot change their lengths.
///
/// Inputs (4):
/// 0: `node_sk` (`PrivateKey`) -- derives `node_id` and signs the body
/// 1: `features` (`Features`)
/// 2: `timestamp` (`Timestamp`)
/// 3: `addresses` (`Bytes`, raw u16-length-prefixed address descriptors)
BuildNodeAnnouncement {
/// 3-byte RGB color for UI display.
rgb_color: [u8; 3],
/// 32-byte node alias, zero-padded.
alias: [u8; 32],
},

// -- Act: side effects against the target --
/// Send an encoded message over the connection.
Expand Down Expand Up @@ -533,6 +549,12 @@ impl fmt::Display for Operation {
Self::DerivePoint => write!(f, "DerivePoint"),
Self::ExtractAcceptChannel(field) => write!(f, "Extract{field}"),
Self::BuildOpenChannel => write!(f, "BuildOpenChannel"),
Self::BuildNodeAnnouncement { rgb_color, alias } => write!(
f,
"BuildNodeAnnouncement{{rgb={}, alias={}}}",
format_hex(rgb_color),
format_hex(alias),
),
Self::SendMessage => write!(f, "SendMessage"),
}
}
Expand All @@ -557,7 +579,9 @@ impl Operation {
Self::LoadTargetPubkeyFromContext | Self::DerivePoint => Some(VariableType::Point),
Self::LoadChainHashFromContext => Some(VariableType::ChainHash),
Self::ExtractAcceptChannel(field) => Some(field.output_type()),
Self::BuildOpenChannel => Some(VariableType::Message),
Self::BuildOpenChannel | Self::BuildNodeAnnouncement { .. } => {
Some(VariableType::Message)
}
Self::SendMessage | Self::MineBlocks(_) => None,
Self::RecvAcceptChannel => Some(VariableType::AcceptChannel),
}
Expand Down Expand Up @@ -610,6 +634,13 @@ impl Operation {
VariableType::Bytes, // upfront_shutdown_script
VariableType::Features, // channel_type
],

Self::BuildNodeAnnouncement { .. } => vec![
VariableType::PrivateKey, // node_sk
VariableType::Features, // features
VariableType::Timestamp, // timestamp
VariableType::Bytes, // addresses
],
}
}

Expand Down Expand Up @@ -638,6 +669,7 @@ impl Operation {
| Self::DerivePoint
| Self::ExtractAcceptChannel(_)
| Self::BuildOpenChannel
| Self::BuildNodeAnnouncement { .. }
| Self::SendMessage
| Self::MineBlocks(_) => vec![],

Expand Down Expand Up @@ -665,8 +697,9 @@ impl Operation {
| Self::LoadChannelId(_)
| Self::LoadShutdownScript(_)
| Self::LoadChannelType(_)
| Self::MineBlocks(_)
| Self::ExtractAcceptChannel(_) => true,
| Self::ExtractAcceptChannel(_)
| Self::BuildNodeAnnouncement { .. }
| Self::MineBlocks(_) => true,

Self::LoadTargetPubkeyFromContext
| Self::LoadChainHashFromContext
Expand Down
88 changes: 88 additions & 0 deletions smite-ir/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,60 @@ fn display_open_channel_program() {
}
}

#[test]
fn display_build_node_announcement_program() {
let mut alias = [0u8; 32];
alias[..5].copy_from_slice(b"smite");
let instructions = vec![
Instruction {
operation: Operation::LoadPrivateKey(key(1)),
inputs: vec![],
},
Instruction {
operation: Operation::LoadFeatures(vec![]),
inputs: vec![],
},
Instruction {
operation: Operation::LoadTimestamp(1_700_000_000),
inputs: vec![],
},
Instruction {
operation: Operation::LoadBytes(vec![]),
inputs: vec![],
},
Instruction {
operation: Operation::BuildNodeAnnouncement {
rgb_color: [0x11, 0x22, 0x33],
alias,
},
inputs: vec![0, 1, 2, 3],
},
Instruction {
operation: Operation::SendMessage,
inputs: vec![4],
},
];

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

let z31 = "00".repeat(31);
let alias_hex = format!("0x736d697465{}", "00".repeat(27));
let expected: Vec<String> = vec![
format!("v0 = LoadPrivateKey(0x{z31}01)"),
"v1 = LoadFeatures()".into(),
"v2 = LoadTimestamp(1700000000)".into(),
"v3 = LoadBytes()".into(),
format!("v4 = BuildNodeAnnouncement{{rgb=0x112233, alias={alias_hex}}}(v0, v1, v2, v3)"),
"SendMessage(v4)".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 Expand Up @@ -1043,6 +1097,40 @@ fn param_mutator_caps_byte_length() {
}
}

#[test]
fn param_mutator_modifies_node_announcement_params() {
let original_rgb = [0x11, 0x22, 0x33];
let original_alias = [0xaa; 32];
let mut builder = ProgramBuilder::new();
let sk = builder.append(Operation::LoadPrivateKey(key(1)), &[]);
let features = builder.append(Operation::LoadFeatures(vec![]), &[]);
let timestamp = builder.append(Operation::LoadTimestamp(0), &[]);
let addresses = builder.append(Operation::LoadBytes(vec![]), &[]);
let build_idx = builder.append(
Operation::BuildNodeAnnouncement {
rgb_color: original_rgb,
alias: original_alias,
},
&[sk, features, timestamp, addresses],
);
let mut program = builder.build();

let mutator = OperationParamMutator;
let mut rng = SmallRng::seed_from_u64(0);

for _ in 0..100 {
mutator.mutate(&mut program, &mut rng);
}

let Operation::BuildNodeAnnouncement { rgb_color, alias } =
&program.instructions[build_idx].operation
else {
panic!("operation type changed");
};
assert_ne!(*rgb_color, original_rgb, "rgb_color never mutated");
assert_ne!(*alias, original_alias, "alias never mutated");
}

#[test]
fn param_mutator_preserves_extract_field_type() {
// One ExtractAcceptChannel instruction per field variant.
Expand Down
116 changes: 115 additions & 1 deletion smite-scenarios/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
//! Executes an IR program against a target node over an established connection,
//! producing side effects (sending/receiving messages).

use bitcoin::secp256k1::ecdsa::Signature;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use smite::bitcoin::BitcoinCli;
use smite::bolt::{
AcceptChannel, ChannelId, Message, OpenChannel, OpenChannelTlvs, Pong, msg_type,
AcceptChannel, ChannelId, Message, NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong,
msg_type,
};
use smite::noise::{ConnectionError, NoiseConnection};
use smite_ir::operation::AcceptChannelField;
Expand Down Expand Up @@ -169,6 +171,12 @@ pub fn execute(
Some(Variable::Message(encoded))
}

Operation::BuildNodeAnnouncement { rgb_color, alias } => {
let na = build_node_announcement(&variables, &instr.inputs, *rgb_color, *alias)?;
let encoded = Message::NodeAnnouncement(na).encode();
Some(Variable::Message(encoded))
}

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

fn resolve_timestamp(variables: &[Option<Variable>], index: usize) -> Result<u32, ExecuteError> {
let var = resolve(variables, index)?;
match var {
Variable::Timestamp(v) => Ok(*v),
_ => Err(type_err(VariableType::Timestamp, var)),
}
}

fn resolve_u16(variables: &[Option<Variable>], index: usize) -> Result<u16, ExecuteError> {
let var = resolve(variables, index)?;
match var {
Expand Down Expand Up @@ -369,6 +385,36 @@ fn build_open_channel(
})
}

/// Builds a signed `NodeAnnouncement` from 4 input variables.
fn build_node_announcement(
variables: &[Option<Variable>],
inputs: &[usize],
rgb_color: [u8; 3],
alias: [u8; 32],
) -> Result<NodeAnnouncement, ExecuteError> {
let sk_bytes = resolve_private_key(variables, inputs[0])?;
let features = resolve_features(variables, inputs[1])?.to_vec();
let timestamp = resolve_timestamp(variables, inputs[2])?;
let addresses = resolve_bytes(variables, inputs[3])?.to_vec();

let sk = SecretKey::from_slice(&sk_bytes).map_err(|_| ExecuteError::InvalidPrivateKey)?;
let secp = Secp256k1::new();
let node_id = PublicKey::from_secret_key(&secp, &sk);

let mut na = NodeAnnouncement {
signature: Signature::from_compact(&[0u8; 64]).expect("zero bytes parse as a signature"),
features,
timestamp,
node_id,
rgb_color,
alias,
addresses,
extra: Vec::new(),
};
na.sign(&sk);
Ok(na)
}

/// 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 @@ -683,6 +729,74 @@ mod tests {
assert!(oc.tlvs.channel_type.is_none());
}

#[test]
fn execute_build_node_announcement() {
let mut sk_bytes = [0u8; 32];
sk_bytes[31] = 0x42;
let rgb_color = [0x11, 0x22, 0x33];
let mut alias = [0u8; 32];
alias[..5].copy_from_slice(b"smite");
let addresses = vec![0xaa, 0xbb, 0xcc];

let instrs = vec![
Instruction {
operation: Operation::LoadPrivateKey(sk_bytes),
inputs: vec![],
},
Instruction {
operation: Operation::LoadFeatures(vec![0x01, 0x02]),
inputs: vec![],
},
Instruction {
operation: Operation::LoadTimestamp(1_700_000_000),
inputs: vec![],
},
Instruction {
operation: Operation::LoadBytes(addresses.clone()),
inputs: vec![],
},
Instruction {
operation: Operation::BuildNodeAnnouncement { rgb_color, alias },
inputs: vec![0, 1, 2, 3],
},
Instruction {
operation: Operation::SendMessage,
inputs: vec![4],
},
];

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 na = match Message::decode(&conn.sent[0]).expect("valid message") {
Message::NodeAnnouncement(na) => na,
other => panic!("expected NodeAnnouncement, got type {}", other.msg_type()),
};

let secp = Secp256k1::new();
let expected_node_id =
PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&sk_bytes).unwrap());
assert_eq!(na.node_id, expected_node_id);
assert_eq!(na.features, vec![0x01, 0x02]);
assert_eq!(na.timestamp, 1_700_000_000);
assert_eq!(na.rgb_color, rgb_color);
assert_eq!(na.alias, alias);
assert_eq!(na.addresses, addresses);
assert!(na.extra.is_empty());
assert!(na.verify());
}

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