1use std::collections::hash_map;
2use std::fmt::Write;
3
4use ahash::HashMap;
5use anyhow::Result;
6use num_bigint::BigInt;
7use smol_str::SmolStr;
8use tycho_types::prelude::*;
9use tycho_vm::{DumpError, DumpOutput, DumpResult};
10
11pub use self::models::{
12 Code, CodeBlock, CodeBlockTail, Data, DataBlock, Item, ItemId, JumpTable, Library, Opcode,
13 OpcodeArg,
14};
15
16mod models;
17mod tokenizer;
18
19pub fn disasm_structured(code: Cell) -> Result<Code> {
20 let mut stack = Vec::new();
21 let mut res = Resources::default();
22
23 let root = match res.add_cont(code) {
25 Ok(item) => {
27 let id = item.id;
28 stack.push(item);
29 id
30 }
31 Err(id) => id,
33 };
34
35 let cp = tycho_vm::codepage0();
37
38 let mut state = DumpState {
40 opcode: String::with_capacity(128),
41 ..Default::default()
42 };
43
44 let mut new_conts = Vec::new();
47 let mut links = Vec::new();
48
49 'outer: while let Some(item) = stack.last_mut() {
50 let mut cs = item.slice.0.apply(&item.slice.1)?;
54
55 state.root = item.slice.1.clone();
58
59 let mut incomplete = false;
60 'inner: while !cs.is_data_empty() {
61 let offset_bits = cs.offset_bits();
63 let offset_refs = cs.offset_refs();
64
65 let dump = cp.dispatch_dump(&mut cs, &mut state);
67 debug_assert_eq!(cs.cell(), item.slice.1.as_ref());
69
70 match dump {
71 Ok(()) => item.slice.0 = cs.range(),
73 Err(DumpError::InvalidOpcode) => {
75 state.clear();
76 incomplete = true;
77 break 'inner;
78 }
79 Err(e) => return Err(e.into()),
81 }
82
83 for cell in state.cells.drain(..) {
85 let id = res.add_cell(cell);
86 links.push(id);
87 }
88
89 for slice in state.slices.drain(..) {
91 let id = res.add_slice(slice);
92 links.push(id);
93 }
94
95 for cont in state.conts.drain(..) {
97 let id = match res.add_cont(cont) {
98 Ok(item) => {
101 let id = item.id;
102 new_conts.push(item);
103 id
104 }
105 Err(id) => id,
107 };
108
109 links.push(id);
110 }
111
112 for cont in state.cont_slices.drain(..) {
114 let id = match res.add_cont_slice(cont) {
115 Ok(item) => {
118 let id = item.id;
119 new_conts.push(item);
120 id
121 }
122 Err(id) => id,
124 };
125
126 links.push(id);
127 }
128
129 for (n, root) in state.dicts.drain(..) {
131 let id = match res.add_jump_table(n, root) {
132 Ok(item) => {
135 let id = item.id;
136 new_conts.extend(item.to_parse);
137 id
138 }
139 Err(id) => id,
141 };
142
143 links.push(id);
144 }
145
146 let (name, args) = res.tokenize(&state.opcode, &links)?;
148
149 item.block.opcodes.push(Opcode {
151 bits: cs.offset_bits() - offset_bits,
152 refs: cs.offset_refs() - offset_refs,
153 name,
154 args,
155 gas: state.gas,
156 });
157
158 state.clear();
160 links.clear();
161
162 if !new_conts.is_empty() {
165 stack.extend(new_conts.drain(..).rev());
166 continue 'outer;
167 }
168 }
169
170 let next = (!incomplete && !cs.is_refs_empty())
172 .then(|| cs.load_reference_cloned())
173 .transpose()?;
174
175 let mut item = stack.pop().unwrap();
177
178 if let Some(next) = next {
179 match res.add_cont(next) {
181 Ok(next_item) => {
183 item.block.tail = Some(CodeBlockTail::Child { id: next_item.id });
184 stack.push(next_item);
185 }
186 Err(next_id) => {
188 item.block.tail = Some(CodeBlockTail::Child { id: next_id });
189 }
190 }
191 } else if incomplete {
192 item.block.tail = Some(CodeBlockTail::Incomplete);
194 } else {
195 item.block.tail = None;
197 }
198
199 res.items[item.id as usize] = Item::Code(item.block);
201
202 if let Some(save_to) = item.save_to {
204 let Item::JumpTable(table) = &mut res.items[save_to.jump_table_id as usize] else {
205 unreachable!("save_to index mismatch");
206 };
207 table.items.push((save_to.key, item.id));
208 }
209 }
210
211 Ok(Code {
213 root,
214 items: res.items,
215 })
216}
217
218#[derive(Default)]
219struct Resources {
220 cont_to_id: HashMap<HashBytes, ItemId>,
221 cont_slice_to_id: HashMap<SliceKey, ItemId>,
222 cell_to_id: HashMap<HashBytes, ItemId>,
223 slice_to_id: HashMap<SliceKey, ItemId>,
224 lib_to_id: HashMap<HashBytes, ItemId>,
225 dict_to_id: HashMap<(u16, HashBytes), ItemId>,
226 items: Vec<Item>,
227}
228
229impl Resources {
230 fn tokenize(&self, text: &str, links: &[ItemId]) -> Result<(SmolStr, Vec<OpcodeArg>)> {
231 use self::tokenizer::{OpcodeArgToken, OpcodeTokens, tokenize};
232
233 let OpcodeTokens { name, args } = tokenize(text)?;
234
235 let mut links = links.iter();
236 let mut res = Vec::with_capacity(args.len());
237 for arg in args {
238 res.push(match arg {
239 OpcodeArgToken::Int(int) => OpcodeArg::Int(int),
240 OpcodeArgToken::Stack { idx } => OpcodeArg::Stack { idx },
241 OpcodeArgToken::Reg { idx } => OpcodeArg::Reg { idx },
242 OpcodeArgToken::Cell => {
243 let Some(id) = links.next().copied() else {
244 anyhow::bail!("not enough cell links");
245 };
246
247 match self.items.get(id as usize) {
248 Some(Item::Data(DataBlock {
250 data: Data::Cell(..),
251 })) => {}
252 Some(Item::Code(code)) => {
254 anyhow::ensure!(
255 !code.is_inline,
256 "cell argument points to an inlined continuation"
257 );
258 }
259 Some(Item::Library(..)) => {}
261 Some(_) => anyhow::bail!("invalid link type"),
263 None => anyhow::bail!("unknown link"),
265 }
266
267 OpcodeArg::Cell { id }
268 }
269 OpcodeArgToken::Slice => {
270 let Some(id) = links.next().copied() else {
271 anyhow::bail!("not enough slice links");
272 };
273
274 match self.items.get(id as usize) {
275 Some(Item::Data(DataBlock {
277 data: Data::Slice(..),
278 })) => {}
279 Some(Item::Code(code)) => {
281 anyhow::ensure!(
282 code.is_inline,
283 "cell argument points to a non-inlined continuation"
284 );
285 }
286 Some(Item::JumpTable(..)) => {}
288 Some(_) => anyhow::bail!("invalid link type"),
290 None => anyhow::bail!("unknown link"),
292 }
293
294 OpcodeArg::Slice { id }
295 }
296 });
297 }
298
299 anyhow::ensure!(links.next().is_none(), "not all links were used");
300 Ok((name, res))
301 }
302
303 fn add_cont(&mut self, cont: Cell) -> Result<DumpStackItem, ItemId> {
304 if cont.descriptor().is_library() {
305 return Err(self.add_library(cont));
306 }
307
308 match self.cont_to_id.entry(*cont.repr_hash()) {
309 hash_map::Entry::Vacant(entry) => {
311 let id = add_item(&mut self.items, DataBlock {
313 data: Cell::default().into(),
314 });
315 entry.insert(id);
316 Ok(DumpStackItem::new(id, cont.into(), false, None))
318 }
319 hash_map::Entry::Occupied(entry) => Err(*entry.get()),
321 }
322 }
323
324 fn add_cont_slice(&mut self, cont: CellSliceParts) -> Result<DumpStackItem, ItemId> {
325 match self.cont_slice_to_id.entry((*cont.1.repr_hash(), cont.0)) {
327 hash_map::Entry::Vacant(entry) => {
328 let id = add_item(&mut self.items, DataBlock {
330 data: CellSliceParts::from(Cell::default()).into(),
331 });
332 entry.insert(id);
333 Ok(DumpStackItem::new(id, cont, true, None))
335 }
336 hash_map::Entry::Occupied(entry) => Err(*entry.get()),
338 }
339 }
340
341 fn add_jump_table(&mut self, n: u16, root: Cell) -> Result<NewJumpTable, ItemId> {
342 match self.dict_to_id.entry((n, *root.repr_hash())) {
343 hash_map::Entry::Vacant(entry) => {
345 let id = add_item(&mut self.items, JumpTable {
347 cell_hash: *root.repr_hash(),
348 key_bits: n,
349 items: Vec::new(),
350 is_full_code: true,
351 });
352 entry.insert(id);
353
354 let mut to_parse = Vec::new();
356 if (|| {
357 for item in tycho_types::dict::RawOwnedIter::new(&Some(root), n) {
358 let (key, value) = item?;
359 let key = key.as_data_slice().load_bigint(n, true)?;
360
361 let save_to = SaveTo {
362 jump_table_id: id,
363 key,
364 };
365
366 let id = add_item(&mut self.items, DataBlock {
367 data: Cell::default().into(),
368 });
369 to_parse.push(DumpStackItem::new(id, value, true, Some(save_to)));
370 }
371 Ok::<_, tycho_types::error::Error>(())
372 })()
373 .is_ok()
374 {
375 let Item::JumpTable(table) = &mut self.items[id as usize] else {
377 unreachable!("table index mismatch");
378 };
379 table.is_full_code = true;
380 }
381
382 Ok(NewJumpTable { id, to_parse })
383 }
384 hash_map::Entry::Occupied(entry) => Err(*entry.get()),
386 }
387 }
388
389 fn add_cell(&mut self, cell: Cell) -> ItemId {
390 *self
391 .cell_to_id
392 .entry(*cell.repr_hash())
393 .or_insert_with(|| add_item(&mut self.items, DataBlock { data: cell.into() }))
394 }
395
396 fn add_slice(&mut self, slice: CellSliceParts) -> ItemId {
397 *self
398 .slice_to_id
399 .entry((*slice.1.repr_hash(), slice.0))
400 .or_insert_with(|| add_item(&mut self.items, DataBlock { data: slice.into() }))
401 }
402
403 fn add_library(&mut self, cell: Cell) -> ItemId {
404 debug_assert!(cell.descriptor().is_library());
405
406 let hash = *cell.repr_hash();
407 *self
408 .lib_to_id
409 .entry(hash)
410 .or_insert_with(|| add_item(&mut self.items, Library { cell_hash: hash }))
411 }
412}
413
414fn add_item<T: Into<Item>>(items: &mut Vec<Item>, item: T) -> ItemId {
415 let id = items.len() as ItemId;
416 items.push(item.into());
417 id
418}
419
420struct DumpStackItem {
421 id: ItemId,
422 block: CodeBlock,
423 slice: CellSliceParts,
424 save_to: Option<SaveTo>,
425}
426
427impl DumpStackItem {
428 fn new(id: ItemId, slice: CellSliceParts, is_inline: bool, save_to: Option<SaveTo>) -> Self {
429 Self {
430 id,
431 block: CodeBlock {
432 cell_hash: *slice.1.repr_hash(),
433 is_inline,
434 offset_bits: slice.0.offset_bits(),
435 offset_refs: slice.0.offset_refs(),
436 bits: slice.0.size_bits(),
437 refs: slice.0.size_refs(),
438 opcodes: Vec::new(),
439 tail: Some(CodeBlockTail::Incomplete),
440 },
441 slice,
442 save_to,
443 }
444 }
445}
446
447struct SaveTo {
448 jump_table_id: ItemId,
449 key: BigInt,
450}
451
452struct NewJumpTable {
453 id: ItemId,
454 to_parse: Vec<DumpStackItem>,
455}
456
457type SliceKey = (HashBytes, CellSliceRange);
458
459#[derive(Default)]
460struct DumpState {
461 root: Cell,
462 cells: Vec<Cell>,
463 slices: Vec<CellSliceParts>,
464 conts: Vec<Cell>,
465 cont_slices: Vec<CellSliceParts>,
466 dicts: Vec<(u16, Cell)>,
467 gas: u64,
468 opcode: String,
469}
470
471impl DumpState {
472 fn clear(&mut self) {
473 self.cells.clear();
474 self.slices.clear();
475 self.conts.clear();
476 self.cont_slices.clear();
477 self.dicts.clear();
478 self.gas = 0;
479 self.opcode.clear();
480 }
481}
482
483impl DumpOutput for DumpState {
484 fn record_gas(&mut self, gas: u64) -> DumpResult {
485 self.gas = gas;
486 Ok(())
487 }
488
489 fn record_opcode(&mut self, value: &dyn std::fmt::Display) -> DumpResult {
490 write!(&mut self.opcode, "{}", value)?;
491 Ok(())
492 }
493
494 fn record_cell(&mut self, value: Cell) -> DumpResult {
495 self.cells.push(value);
496 Ok(())
497 }
498
499 fn record_slice(&mut self, value: CellSlice<'_>) -> DumpResult {
500 if value.cell() != self.root.as_ref() {
501 return Err(DumpError::CellMismatch);
502 }
503 self.slices.push((value.range(), self.root.clone()));
504 Ok(())
505 }
506
507 fn record_cont(&mut self, cont: Cell) -> DumpResult {
508 self.conts.push(cont);
509 Ok(())
510 }
511
512 fn record_cont_slice(&mut self, cont: CellSlice<'_>) -> DumpResult {
513 if cont.cell() != self.root.as_ref() {
514 return Err(DumpError::CellMismatch);
515 }
516 self.cont_slices.push((cont.range(), self.root.clone()));
517 Ok(())
518 }
519
520 fn record_dict(&mut self, n: u16, mut slice: CellSlice<'_>) -> DumpResult {
521 self.dicts.push((n, slice.load_reference_cloned()?));
522 Ok(())
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use anyhow::Result;
529 use tycho_asm_macros::tvmasm;
530
531 use super::*;
532
533 #[test]
534 fn disasm_ever_wallet() -> Result<()> {
535 let code = Boc::decode_base64(
536 "te6ccgEBBgEA/AABFP8A9KQT9LzyyAsBAgEgBQIC5vJx1wEBwADyeoMI1xjtRNCDB9cB1ws/yPgozxYjzxbJ+QADcdcBAcMAmoMH1wFRE7ry4GTegEDXAYAg1wGAINcBVBZ1+RDyqPgju/J5Zr74I4EHCKCBA+ioUiC8sfJ0AiCCEEzuZGy64w8ByMv/yz/J7VQEAwA+ghAWnj4Ruo4R+AACkyDXSpd41wHUAvsA6NGTMvI84gCYMALXTND6QIMG1wFx1wF41wHXTPgAcIAQBKoCFLHIywVQBc8WUAP6AstpItAhzzEh10mghAm5mDNwAcsAWM8WlzBxAcsAEsziyQH7AAAE0jA=",
537 )?;
538 let code = disasm_structured(code)?;
539 println!("{}", serde_json::to_string_pretty(&code).unwrap());
540
541 Ok(())
548 }
549
550 #[test]
551 fn disasm_wallet_v3() -> Result<()> {
552 let code = Boc::decode_base64(
553 "te6ccgEBAQEAcQAA3v8AIN0gggFMl7ohggEznLqxn3Gw7UTQ0x/THzHXC//jBOCk8mCDCNcYINMf0x/TH/gjE7vyY+1E0NMf0x/T/9FRMrryoVFEuvKiBPkBVBBV+RDyo/gAkyDXSpbTB9QC+wDo0QGkyMsfyx/L/8ntVA==",
554 )?;
555 let code = disasm_structured(code)?;
556 println!("{}", serde_json::to_string_pretty(&code).unwrap());
557
558 Ok(())
565 }
566
567 #[test]
568 fn disasm_slice_data() -> Result<()> {
569 let code = Boc::decode(tvmasm!("PUSHSLICE x{6_}"))?;
570 let code = disasm_structured(code)?;
571 println!("{}", serde_json::to_string_pretty(&code).unwrap());
572
573 Ok(())
574 }
575
576 #[test]
577 fn disasm_cell_data() -> Result<()> {
578 let code = Boc::decode(tvmasm!("ACCEPT INT 123 PUSHREF x{6_}"))?;
579 println!("{}", Boc::encode_base64(&code));
580 let code = disasm_structured(code)?;
581 println!("{}", serde_json::to_string_pretty(&code).unwrap());
582
583 Ok(())
584 }
585
586 #[test]
587 fn disasm_elector() -> Result<()> {
588 let code = Boc::decode(include_bytes!("../res/elector_code.boc"))?;
589 let _code = disasm_structured(code)?;
590 Ok(())
595 }
596}