Skip to content

Commit 7337a26

Browse files
committed
Fix: Undo until all ifs of the chunk are closed
1 parent 079dcd1 commit 7337a26

File tree

3 files changed

+165
-44
lines changed

3 files changed

+165
-44
lines changed

src/builder.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ impl StructuredScript {
7272
self.size
7373
}
7474

75+
pub fn contains_flow_op(&self) -> bool {
76+
!(self.unclosed_if_positions.is_empty() && self.extra_endif_positions().is_empty() && self.max_if_interval == (0,0))
77+
}
78+
79+
pub fn is_script_buf(&self) -> bool {
80+
self.blocks.len() == 1 && matches!(self.blocks[0], Block::Script(_))
81+
}
82+
7583
pub fn num_unclosed_ifs(&self) -> i32 {
7684
self.num_unclosed_ifs
7785
}

src/chunker.rs

Lines changed: 134 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
use core::panic;
2+
3+
use bitcoin::{opcodes::all::{OP_ENDIF, OP_IF, OP_NOTIF}, script::Instruction, ScriptBuf};
4+
15
use crate::{
26
analyzer::StackStatus,
37
builder::{Block, StructuredScript},
@@ -12,12 +16,34 @@ struct ChunkStats {
1216
altstack_output_size: usize,
1317
}
1418

15-
1619
//TODO: Refactor the undoing with this struct
17-
//pub struct UndoInfo {
18-
//
19-
//
20-
//}
20+
pub struct UndoInfo {
21+
call_stack: Vec<Box<StructuredScript>>,
22+
size: usize,
23+
num_unclosed_ifs: i32,
24+
}
25+
26+
impl UndoInfo {
27+
pub fn new() -> Self {
28+
Self {
29+
call_stack: vec![],
30+
size: 0,
31+
num_unclosed_ifs: 0,
32+
}
33+
}
34+
35+
pub fn reset(&mut self) -> Vec<Box<StructuredScript>> {
36+
self.size = 0;
37+
self.num_unclosed_ifs = 0;
38+
std::mem::take(&mut self.call_stack)
39+
}
40+
41+
pub fn update(&mut self, builder: StructuredScript) {
42+
self.size += builder.len();
43+
self.num_unclosed_ifs += builder.num_unclosed_ifs();
44+
self.call_stack.push(Box::new(builder));
45+
}
46+
}
2147

2248
#[derive(Debug, Clone)]
2349
pub struct Chunk {
@@ -40,12 +66,12 @@ impl Chunk {
4066
}
4167
}
4268

69+
#[derive(Debug)]
4370
pub struct Chunker {
4471
// Each chunk has to be in the interval [target_chunk_size - tolerance, target_chunk_size]
4572
target_chunk_size: usize,
4673
tolerance: usize,
4774

48-
size: usize,
4975
pub chunks: Vec<Chunk>,
5076

5177
// Builder Callstack (consists of remaining structured scripts)
@@ -61,7 +87,6 @@ impl Chunker {
6187
Chunker {
6288
target_chunk_size,
6389
tolerance,
64-
size: top_level_script.len(),
6590
chunks: vec![],
6691
call_stack: vec![Box::new(top_level_script)],
6792
}
@@ -98,16 +123,90 @@ impl Chunker {
98123
stack_analyzer.analyze_blocks(chunk)
99124
}
100125

126+
pub fn undo(
127+
&mut self,
128+
mut num_unclosed_ifs: i32, //TODO: We should be able to use undo_info.num_unclosed_ifs
129+
mut undo_info: UndoInfo,
130+
) -> (Vec<Box<StructuredScript>>, usize) {
131+
if num_unclosed_ifs == 0 {
132+
return (vec![], 0);
133+
}
134+
135+
println!("[INFO] Unable to close all ifs. Undoing the added scripts to a point where num_unclosed_ifs is 0.");
136+
println!("[INFO] UNDO CALL STACK: {:?}", undo_info.call_stack);
137+
let mut removed_scripts = vec![];
138+
let mut removed_len = 0;
139+
140+
loop {
141+
let builder = match undo_info.call_stack.pop() {
142+
Some(builder) => builder,
143+
None => break, // the last block in the call stack
144+
};
145+
if builder.contains_flow_op() {
146+
if builder.is_script_buf() {
147+
num_unclosed_ifs -= builder.num_unclosed_ifs();
148+
println!("[INFO] Removing {:?}", builder.blocks);
149+
removed_len += builder.len();
150+
removed_scripts.push(builder);
151+
if num_unclosed_ifs == 0 {
152+
break;
153+
}
154+
} else {
155+
for block in builder.blocks.iter().rev() {
156+
match block {
157+
Block::Call(id) => {
158+
let sub_builder = builder.script_map.get(&id).unwrap();
159+
undo_info.call_stack.push(Box::new(sub_builder.clone()));
160+
}
161+
Block::Script(script_buf) => {
162+
//TODO: Can we avoid cloning or creating a builder here?
163+
// Split the script_buf at OP_IF/OP_NOTIF and OP_ENDIF
164+
let mut tmp_script = ScriptBuf::new();
165+
for instruction_res in script_buf.instructions() {
166+
let instruction = instruction_res.unwrap();
167+
match instruction {
168+
Instruction::Op(OP_IF) | Instruction::Op(OP_ENDIF) | Instruction::Op(OP_NOTIF) => {
169+
undo_info.call_stack.push(Box::new(
170+
StructuredScript::new("").push_script(std::mem::take(&mut tmp_script)),
171+
));
172+
tmp_script.push_instruction_no_opt(instruction);
173+
undo_info.call_stack.push(Box::new(
174+
StructuredScript::new("").push_script(std::mem::take(&mut tmp_script)),
175+
));
176+
}
177+
_ => tmp_script.push_instruction_no_opt(instruction),
178+
179+
}
180+
}
181+
}
182+
}
183+
}
184+
}
185+
} else {
186+
// No OP_IF, OP_NOTIF or OP_ENDIF in that structured script so just remove it
187+
println!(
188+
"[INFO] Removing because it contains no flow control {:?}",
189+
builder
190+
);
191+
removed_len += builder.len();
192+
removed_scripts.push(builder);
193+
}
194+
}
195+
196+
self.call_stack.extend(removed_scripts.into_iter().rev());
197+
assert!(num_unclosed_ifs >= 0, "More OP_ENDIF's than OP_IF's after undo step. (This means there is a bug in the undo logic.)");
198+
assert_eq!(num_unclosed_ifs, 0, "Unable to make up for the OP_IF's in this chunk. Consider a larger target size or more tolerance. Unclosed OP_IF's: {:?}, removed_len: {}, undo.call_stack: {:?}", num_unclosed_ifs, removed_len, undo_info.call_stack);
199+
(undo_info.call_stack, removed_len)
200+
}
201+
101202
fn find_next_chunk(&mut self) -> Chunk {
102203
let mut chunk_scripts = vec![];
103204
let mut chunk_len = 0;
104205
let mut num_unclosed_ifs = 0;
105206

106207
// All not selected StructuredScripts that have to be added to the call_stack again
107-
let mut call_stack_undo = vec![];
108-
let mut chunk_len_undo = 0;
109-
let mut num_unclosed_ifs_undo = 0;
110-
208+
let mut undo_info = UndoInfo::new();
209+
111210
let max_depth = 8;
112211
let mut depth = 0;
113212

@@ -122,7 +221,9 @@ impl Chunker {
122221

123222
assert!(
124223
num_unclosed_ifs + builder.num_unclosed_ifs() >= 0,
125-
"More OP_ENDIF's than OP_IF's in the script. num_unclosed_if: {:?}, builder: {:?}", num_unclosed_ifs, builder.num_unclosed_ifs()
224+
"More OP_ENDIF's than OP_IF's in the script. num_unclosed_if: {:?}, builder: {:?}",
225+
num_unclosed_ifs,
226+
builder.num_unclosed_ifs()
126227
);
127228

128229
// TODO: Use stack analysis to find best possible chunk border
@@ -135,32 +236,27 @@ impl Chunker {
135236
} else if chunk_len + block_len <= self.target_chunk_size {
136237
// Case 2: Adding the current builder remains a valid solution.
137238
// TODO: Check with stack analyzer to see if adding the builder is better or not.
138-
let script_unclosed_ifs = builder.num_unclosed_ifs();
239+
num_unclosed_ifs += builder.num_unclosed_ifs();
240+
chunk_len += block_len;
139241
if num_unclosed_ifs + builder.num_unclosed_ifs() == 0 {
140242
// We are going to keep this structured script in the chunk
141-
chunk_scripts.extend(call_stack_undo);
142-
chunk_scripts.push(Box::new(builder));
143243
// Reset the undo information
144-
call_stack_undo = vec![];
145-
chunk_len_undo = 0;
146-
num_unclosed_ifs_undo = 0;
244+
chunk_scripts.extend(undo_info.reset());
245+
chunk_scripts.push(Box::new(builder));
147246
} else {
148247
// Update the undo information in case we need to remove this StructuredScript
149248
// from the chunk again
150-
call_stack_undo.push(Box::new(builder));
151-
chunk_len_undo += block_len;
152-
num_unclosed_ifs_undo += script_unclosed_ifs;
249+
undo_info.update(builder);
153250
}
251+
// Reset the depth parameter
154252
depth = 0;
155-
num_unclosed_ifs += script_unclosed_ifs;
156-
chunk_len += block_len;
157253
} else if chunk_len + block_len > self.target_chunk_size
158-
&& (chunk_len < self.target_chunk_size - self.tolerance || chunk_len == 0 || depth <= max_depth)
254+
&& (chunk_len < self.target_chunk_size - self.tolerance
255+
|| chunk_len == 0
256+
|| depth <= max_depth)
159257
{
160-
//println!("[INFO] Chunking a call now.");
161258
// Case 3: Current builder too large and there is no acceptable solution yet
162-
// TODO: Could add a depth parameter here to even if we have an acceptable solution
163-
// check if there is a better one in next depth calls
259+
// Even if we have an acceptable solution we check if there is a better one in next depth calls
164260
// Chunk inside a call of the current builder.
165261
// Add all its calls to the call_stack.
166262
let mut contains_call = false;
@@ -181,28 +277,23 @@ impl Chunker {
181277
}
182278
}
183279
}
184-
assert!(contains_call || depth <= max_depth, "No support for chunking up ScriptBufs, depth: {}", depth);
280+
assert!(
281+
contains_call || depth <= max_depth,
282+
"No support for chunking up ScriptBufs, depth: {}",
283+
depth
284+
);
185285
depth += 1;
186286
} else {
187-
call_stack_undo.push(Box::new(builder));
287+
self.call_stack.push(Box::new(builder));
188288
break;
189289
}
190290
}
191291

192-
// Undo the lately added scripts if we are not closing all ifs with them.
193-
// TODO: This is an issue because we may remove way more than necessary.
194-
if num_unclosed_ifs != 0 {
195-
println!("[INFO] Unable to close all ifs. Undoing the added scripts to the point where num_unclosed_ifs was 0.");
196-
num_unclosed_ifs -= num_unclosed_ifs_undo;
197-
chunk_len -= chunk_len_undo;
198-
}
199-
200-
assert!(num_unclosed_ifs >= 0, "More OP_ENDIF's than OP_IF's after undo step. (This means there is a bug in the undo logic.)");
201-
assert_eq!(num_unclosed_ifs, 0, "Unable to make up for the OP_IF's in this chunk. Consider a larger target size or more tolerance. Unclosed OP_IF's: {:?}", num_unclosed_ifs);
202-
203-
// Always have to do this because of the last call_stack element we popped that did not end up in
204-
// the chunk.
205-
self.call_stack.extend(call_stack_undo.into_iter().rev());
292+
// Undo the lately added scripts until the num_unclosed_ifs is 0.
293+
let undo_result = self.undo(num_unclosed_ifs, undo_info);
294+
chunk_scripts.extend(undo_result.0);
295+
chunk_len -= undo_result.1;
296+
206297
Chunk::new(chunk_scripts, chunk_len)
207298
}
208299

tests/test_chunker.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,31 @@ fn test_chunker_simple() {
2121

2222
assert_eq!(chunk_borders, vec![2, 2, 2, 2]);
2323
}
24+
#[test]
25+
fn test_chunker_ifs_1() {
26+
let sub_script = script! {
27+
OP_ADD
28+
OP_ADD
29+
};
30+
31+
let script = script! {
32+
{ sub_script.clone() }
33+
OP_IF
34+
{ sub_script.clone() }
35+
OP_ENDIF
36+
};
37+
38+
println!("{:?}", sub_script);
39+
40+
let mut chunker = Chunker::new(script, 5, 4);
41+
let chunk_borders = chunker.find_chunks();
42+
println!("Chunker: {:?}", chunker);
43+
44+
assert_eq!(chunk_borders, vec![2, 4]);
45+
}
2446

2547
#[test]
26-
fn test_chunker_ifs() {
48+
fn test_chunker_ifs_2() {
2749
let sub_script = script! {
2850
OP_ADD
2951
OP_ADD

0 commit comments

Comments
 (0)