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
48 changes: 46 additions & 2 deletions smite-ir/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,36 @@ impl ProgramBuilder {
actual_type, expected_type,
"{operation:?} input {i}: expected {expected_type:?}, got {actual_type:?}",
);
if actual_type.is_affine() {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some trickiness here -- should is_affine() return true or false for a compound variable containing an affine field?

If it returns true, we need to ensure below that we only remove the candidate when the affine field is actually extracted -- not when a non-affine field is extracted.

If it returns false, this code will never run for compound variables, and we would fail to detect multiple uses of affine fields.

Or if we are going to ban affine types from being extractable fields, we should add an assertion somewhere to panic on such cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compound variables containing an affine type should definitely return false on is_affine(), to allow multiple extractions from them.

To access an affine variable contained in a compound one, Generators should first manually Extract* the affine variable, registering it as an available candidate. We can then detect over-extractions by re-introducing the OverExtract logic we just got rid of, and over-use of the extracted affine will be naturally handled by the existing code.

Or, we can introduce dummy operations that accept such compound variables and output a single affine type.

In any case, I don't think we need to deal with this for now. The specifics can be ironed out if and when we encounter such a scenario (I personally have a feeling we won't).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then let's ban it and add an assertion in the operation.extractable_fields() loop below that extractable fields are never affine.

let cands = self.candidates.get_mut(&actual_type).unwrap_or_else(|| {
panic!("{operation:?} input {i}: no candidates for {actual_type:?}")
});

// Find the position, or panic if it doesn't exist.
let pos = cands
.iter()
.position(|c| match c {
Candidate::Direct(idx) => *idx == input_idx,
Candidate::Extract { compound_idx, .. } => *compound_idx == input_idx,
})
.unwrap_or_else(|| {
panic!("{operation:?} input {i}: affine {actual_type:?} already consumed")
});

// Consume it.
cands.remove(pos);
}
}

let idx = self.instructions.len();

if let Some(out_type) = operation.output_type() {
// Register extractable fields for compound types.
for (extract_op, field_type) in operation.extractable_fields() {
assert!(
!out_type.is_affine(),
"Affine variables cannot be extracted"
);
self.candidates
.entry(field_type)
.or_default()
Expand All @@ -103,12 +126,27 @@ impl ProgramBuilder {

/// Selects or creates a variable of the given type using probabilistic
/// variable selection (75% most recent, 15% any existing, 10% fresh).
#[allow(clippy::missing_panics_doc)] // candidates is always non-empty
///
/// # Panics
///
/// Panics in the following scenarios:
/// * An affine type is requested but its candidate pool is empty (all instances have been consumed).
/// * Attempts to generate a fresh variable for a type that cannot be instantiated out-of-thin-air
/// (e.g., `Message`, `AcceptChannel`, or affine types).
/// * Resolving the chosen candidate triggers a panic in the underlying `append` operation.
pub fn pick_variable(&mut self, var_type: VariableType, rng: &mut impl Rng) -> usize {
let Some(candidates) = self.candidates.get(&var_type) else {
let Some(candidates) = self.candidates.get_mut(&var_type) else {
return self.generate_fresh(var_type, rng);
};

if var_type.is_affine() {
let chosen_candidate = candidates
.last()
.unwrap_or_else(|| panic!("no candidates for {var_type:?}"))
.clone();
return self.resolve_candidate(chosen_candidate);
}
Comment on lines +143 to +148
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typical usage of ProgramBuilder is like this:

let v = builder.pick_variable(SentOpenChannel, rng);
builder.append(RecvAcceptChannel, &[v]);

So we can't remove from self.candidates in both pick_variable and append or we'll double-consume the affine variable.

I'm pretty sure we want append to be the only place where we actually mark the variable as consumed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can't remove from self.candidates in both pick_variable and append or we'll double-consume the affine variable.

I'm pretty sure we want append to be the only place where we actually mark the variable as consumed.

Ugh, you're right, my bad.


let roll = rng.random_range(0..20);
match roll {
// 10%: generate a fresh value even though candidates exist.
Expand Down Expand Up @@ -168,12 +206,18 @@ impl ProgramBuilder {
VariableType::Message => {
panic!("cannot generate fresh Message: requires composed inputs")
}
VariableType::OpenChannelMessage => {
panic!("cannot generate fresh OpenChannelMessage: requires composed inputs")
}
VariableType::AcceptChannel => {
panic!("cannot generate fresh AcceptChannel: requires protocol interaction")
}
VariableType::FundingTransaction => {
panic!("cannot generate fresh FundingTransaction: requires composed inputs")
}
VariableType::SentOpenChannel => {
panic!("cannot generate fresh affine type")
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions smite-ir/src/generators/open_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Generator for OpenChannelGenerator {
let channel_type = builder.append(Operation::LoadChannelType(variant), &[]);

// Build and send open_channel.
let msg = builder.append(
let open_channel_msg = builder.append(
Operation::BuildOpenChannel,
&[
chain_hash,
Expand All @@ -73,9 +73,9 @@ impl Generator for OpenChannelGenerator {
channel_type,
],
);
builder.append(Operation::SendMessage, &[msg]);
let sent_open_channel = builder.append(Operation::SendOpenChannel, &[open_channel_msg]);

// Receive accept_channel.
builder.append(Operation::RecvAcceptChannel, &[]);
builder.append(Operation::RecvAcceptChannel, &[sent_open_channel]);
}
}
11 changes: 10 additions & 1 deletion smite-ir/src/mutators/input_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ impl Mutator for InputSwapMutator {
.instructions
.iter()
.enumerate()
.flat_map(|(i, instr)| (0..instr.inputs.len()).map(move |j| (i, j)))
.flat_map(|(i, instr)| {
instr
.operation
.input_types()
.into_iter()
.enumerate()
// Affine variables carry no data, so swapping one affine variable with
// another has no practical effect.
.filter_map(move |(j, ty)| (!ty.is_affine()).then_some((i, j)))
})
Comment on lines +22 to +31
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's a bit awkward/inefficient to call input_types once for every input instead of once per instruction

Suggested change
.flat_map(|(i, instr)| {
(0..instr.inputs.len())
// Affine variables carry no data, so swapping one affine variable with
// another has no practical effect.
.filter(move |&j| !instr.operation.input_types()[j].is_affine())
.map(move |j| (i, j))
})
.flat_map(|(i, instr)| {
instr
.operation
.input_types()
.into_iter()
.enumerate()
.filter_map(move |(j, ty)| (!ty.is_affine()).then_some((i, j)))
})

.choose(rng)
else {
return false;
Expand Down
3 changes: 2 additions & 1 deletion smite-ir/src/mutators/operation_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool {
| Operation::BuildChannelAnnouncement
| Operation::SendMessage
| Operation::RecvAcceptChannel
| Operation::BroadcastTransaction => {
| Operation::BroadcastTransaction
| Operation::SendOpenChannel => {
unreachable!("is_param_mutable returned true for {op:?}")
}
}
Expand Down
25 changes: 18 additions & 7 deletions smite-ir/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ pub enum Operation {
/// Send an encoded message over the connection.
/// Input: `Message`.
SendMessage,
/// Send an `open_channel` message over the connection.
/// Produces a `SentOpenChannel` variable.
/// Input: `OpenChannelMessage`.
SendOpenChannel,
Comment thread
morehouse marked this conversation as resolved.
/// Receive and parse an `accept_channel` response.
/// Produces an `AcceptChannel` compound variable.
RecvAcceptChannel,
Expand Down Expand Up @@ -590,6 +594,7 @@ impl fmt::Display for Operation {
format_hex(alias),
),
Self::SendMessage => write!(f, "SendMessage"),
Self::SendOpenChannel => write!(f, "SendOpenChannel"),
}
}
}
Expand All @@ -615,10 +620,12 @@ impl Operation {
Self::LoadChainHashFromContext => Some(VariableType::ChainHash),
Self::ExtractAcceptChannel(field) => Some(field.output_type()),
Self::CreateFundingTransaction => Some(VariableType::FundingTransaction),
Self::BuildOpenChannel
| Self::BuildChannelAnnouncement
| Self::BuildNodeAnnouncement { .. } => Some(VariableType::Message),
Self::BuildChannelAnnouncement | Self::BuildNodeAnnouncement { .. } => {
Some(VariableType::Message)
}
Self::BuildOpenChannel => Some(VariableType::OpenChannelMessage),
Self::SendMessage | Self::MineBlocks(_) | Self::BroadcastTransaction => None,
Self::SendOpenChannel => Some(VariableType::SentOpenChannel),
Self::RecvAcceptChannel => Some(VariableType::AcceptChannel),
}
}
Expand All @@ -642,9 +649,9 @@ impl Operation {
| Self::LoadChannelType(_)
| Self::LoadTargetPubkeyFromContext
| Self::LoadChainHashFromContext
| Self::MineBlocks(_)
| Self::RecvAcceptChannel => vec![],
| Self::MineBlocks(_) => vec![],

Self::RecvAcceptChannel => vec![VariableType::SentOpenChannel],
Self::DerivePoint => vec![VariableType::PrivateKey],
Self::ExtractAcceptChannel(_) => vec![VariableType::AcceptChannel],
Self::CreateFundingTransaction => vec![
Expand All @@ -656,6 +663,8 @@ impl Operation {
Self::SendMessage => vec![VariableType::Message],
Self::BroadcastTransaction => vec![VariableType::FundingTransaction],

Self::SendOpenChannel => vec![VariableType::OpenChannelMessage],

Self::BuildOpenChannel => vec![
VariableType::ChainHash, // chain_hash
VariableType::ChannelId, // temporary_channel_id
Expand Down Expand Up @@ -729,7 +738,8 @@ impl Operation {
| Self::BuildNodeAnnouncement { .. }
| Self::SendMessage
| Self::MineBlocks(_)
| Self::BroadcastTransaction => vec![],
| Self::BroadcastTransaction
| Self::SendOpenChannel => vec![],

Self::RecvAcceptChannel => AcceptChannelField::ALL
.iter()
Expand Down Expand Up @@ -768,7 +778,8 @@ impl Operation {
| Self::BuildChannelAnnouncement
| Self::SendMessage
| Self::RecvAcceptChannel
| Self::BroadcastTransaction => false,
| Self::BroadcastTransaction
| Self::SendOpenChannel => false,
}
}
}
19 changes: 19 additions & 0 deletions smite-ir/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ pub enum ValidateError {
/// Actual byte length.
len: usize,
},
/// An affine variable is used more than once.
#[error("Variable {index}: affine {var_type:?} consumed twice (max 1)")]
AffineOverUse {
/// The overused affine variable index.
index: usize,
/// Type of the affine variable.
var_type: VariableType,
},
}

impl Program {
Expand All @@ -101,6 +109,8 @@ impl Program {
///
/// Returns the first violation encountered.
pub fn validate(&self) -> Result<(), ValidateError> {
let mut affine_consumed = vec![false; self.instructions.len()];

for (instr_idx, instr) in self.instructions.iter().enumerate() {
let expected = instr.operation.input_types();
if instr.inputs.len() != expected.len() {
Expand Down Expand Up @@ -135,6 +145,15 @@ impl Program {
got: actual_type,
});
}
if expected_type.is_affine() {
if affine_consumed[input_idx] {
return Err(ValidateError::AffineOverUse {
index: input_idx,
var_type: expected_type,
});
}
affine_consumed[input_idx] = true;
}
}
if let Operation::LoadBytes(b) | Operation::LoadFeatures(b) = &instr.operation
&& b.len() > MAX_MESSAGE_SIZE
Expand Down
Loading