Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support any layout BoC #56

Merged
merged 7 commits into from
Feb 27, 2025
Merged
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ yarn add @tact-lang/opcode

## Usage

For most cases you will want to disassemble a BoC file generated by the Tact/FunC/Tolk compiler.
For most cases you will want to disassemble a BoC file generated by the Tact/FunC/Tolk compiler. In this case decompiler will unpack the dictionary to procedures and methods.

```typescript
import {AssemblyWriter, disassembleRoot} from "@tact-lang/opcode"
Expand All @@ -20,12 +20,12 @@ const program = disassembleRoot(source, {
computeRefs: false,
})

// Write the program AST into a TVM bytecode string
// Write the program AST into a Fift assembly string
const res = AssemblyWriter.write(program, {})
console.log(res)
```

If you want to decompile BoC file with non-standard root layout (for example, wallet v1), you can do the following:
If you want to decompile BoC file without unpacking of the dictionary, you can do the following:

```typescript
import {AssemblyWriter, disassembleRawRoot} from "@tact-lang/opcode"
Expand Down
9 changes: 0 additions & 9 deletions src/decompiler/constants.ts

This file was deleted.

95 changes: 56 additions & 39 deletions src/decompiler/disasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ import {
ProgramNode,
} from "../ast/ast"
import {createBlock, createInstruction} from "../ast/helpers"
import {RootLayout} from "./constants"
import {getDisplayNumber, hasHint} from "../spec/helpers"
import {LayoutError, OperandError, UnknownOperandTypeError} from "./errors"
import {OperandError, UnknownOperandTypeError} from "./errors"

export interface DisassembleParams {
/**
Expand Down Expand Up @@ -338,45 +337,52 @@ function processRefOrSliceOperand(
}
}

/**
* Checks if the root layout is valid.
*
* Valid layout is:
* - `SETCP`
* - `DICTPUSHCONST`
* - `DICTIGETJMPZ`
* - `THROWARG`
*
* This is the only layout that is supported by the decompiler.
* This layout is generated by the FunC and Tact compilers.
*/
function checkLayout(opcodes: DecompiledInstruction[]): void {
if (opcodes.length !== RootLayout.instructionsCount) {
throw new LayoutError(RootLayout.instructionsCount, opcodes.length, {
instructions: opcodes.map(op => op.op.definition.mnemonic),
function findDictOpcode(opcodes: DecompiledInstruction[]): DecompiledInstruction | undefined {
return opcodes.find(it => it.op.definition.mnemonic === "DICTPUSHCONST")
}

function findRootMethods(opcodes: DecompiledInstruction[]): MethodNode[] {
const methods: MethodNode[] = []

if (opcodes[2]?.op.definition.mnemonic === "PUSHCONT") {
const cont = opcodes[2].op.operands.at(0)
if (!cont || cont.type !== "subslice") {
return methods
}

const recvInternal = disassembleRawRoot(cont.value)
methods.push({
type: "method",
hash: recvInternal.hash,
offset: recvInternal.offset,
body: recvInternal,
id: 0,
})
}

const isValidLayout =
opcodes[RootLayout.instructions.SETCP].op.definition.mnemonic === "SETCP" &&
opcodes[RootLayout.instructions.DICTPUSHCONST].op.definition.mnemonic === "DICTPUSHCONST" &&
(opcodes[RootLayout.instructions.DICTIGETJMPZ].op.definition.mnemonic === "DICTIGETJMPZ" ||
opcodes[RootLayout.instructions.DICTIGETJMPZ].op.definition.mnemonic ===
"DICTIGETJMP") &&
opcodes[RootLayout.instructions.THROWARG].op.definition.mnemonic === "THROWARG"

if (!isValidLayout) {
throw new LayoutError(RootLayout.instructionsCount, opcodes.length, {
expected: ["SETCP", "DICTPUSHCONST", "DICTIGETJMPZ", "THROWARG"],
actual: opcodes.map(op => op.op.definition.mnemonic),
if (opcodes[6]?.op.definition.mnemonic === "PUSHCONT") {
const cont = opcodes[6].op.operands.at(0)
if (!cont || cont.type !== "subslice") {
return methods
}

const recvExternal = disassembleRawRoot(cont.value)
methods.push({
type: "method",
hash: recvExternal.hash,
offset: recvExternal.offset,
body: recvExternal,
id: -1,
})
}

return methods
}

/**
* Disassembles the root cell into a list of instructions.
*
* Use this function if you want to disassemble the whole BoC file.
* Use this function if you want to disassemble the whole BoC file with dictionary unpacked.
*/
export function disassembleRoot(
cell: Cell,
Expand All @@ -388,29 +394,40 @@ export function disassembleRoot(
},
): ProgramNode {
const opcodes = disassemble({source: cell})
checkLayout(opcodes)

const dictOpcode = opcodes[RootLayout.instructions.DICTPUSHCONST].op
const {procedures, methods} = deserializeDict(dictOpcode.operands, options.computeRefs)

const args = {
source: cell,
offset: {bits: 0, refs: 9},
onCellReference: undefined,
}

const rootMethods = findRootMethods(opcodes)

const dictOpcode = findDictOpcode(opcodes)
if (!dictOpcode) {
// Likely some non-Tact/FunC produced BoC
return {
type: "program",
topLevelInstructions: opcodes.map(op => processInstruction(op, args)),
procedures: [],
methods: rootMethods,
withRefs: options.computeRefs,
}
}

const {procedures, methods} = deserializeDict(dictOpcode.op.operands, options.computeRefs)

return {
type: "program",
topLevelInstructions: opcodes.map(op => processInstruction(op, args)),
procedures,
methods,
methods: [...rootMethods, ...methods],
withRefs: options.computeRefs,
}
}

/**
* Disassembles a cell without any additional checks for the layout.
*
* Use this function if your contract use non-usual layout.
* Disassembles a cell without any additional unpacking of the dictionary.
*/
export function disassembleRawRoot(cell: Cell): BlockNode {
return disassembleAndProcess({
Expand Down
11 changes: 0 additions & 11 deletions src/decompiler/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,6 @@ export class OperandError extends DisassemblerError {
}
}

export class LayoutError extends DisassemblerError {
public constructor(expected: number, actual: number, details?: Record<string, unknown>) {
super(`Unexpected root layout: expected ${expected} instructions, got ${actual}`, {
expected,
actual,
...details,
})
this.name = "LayoutError"
}
}

export class UnknownOperandTypeError extends DisassemblerError {
public constructor(operand: OperandValue, details?: Record<string, unknown>) {
super(`Unknown operand type: ${operand.type}`, {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ export type {AssemblyWriterOptions} from "./printer/assembly-writer"
export {AssemblyWriter} from "./printer/assembly-writer"

export {debugSymbols} from "./utils/known-methods"
export {Cell} from "@ton/core"
export {Cell, Dictionary} from "@ton/core"
22 changes: 14 additions & 8 deletions src/printer/assembly-writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,21 @@ export class AssemblyWriter {
}
}

// if (node.topLevelInstructions.length > 0) {
// node.topLevelInstructions.forEach(instruction => {
// // if (i === 1) return
// this.writer.write("// ")
// this.writeInstructionNode(instruction)
// })
// }

this.writer.writeLine(`"Asm.fif" include`)

if (node.procedures.length === 0 && node.methods.length === 0) {
this.writer.writeLine("<{")

this.writer.indent(() => {
node.topLevelInstructions.forEach(instruction => {
this.writeInstructionNode(instruction)
})
})

this.writer.write("}>c")
return
}

this.writer.writeLine("PROGRAM{")
this.writer.indent(() => {
const methods = [...node.methods].sort((a, b) => a.id - b.id)
Expand Down
148 changes: 148 additions & 0 deletions src/test/e2e/__snapshots__/known-contracts.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,153 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`known contracts > should decompile Tact 1.6.0 with other layout 1`] = `
""Asm.fif" include
PROGRAM{
DECLPROC recv_internal
78250 DECLMETHOD ?fun_78250
DECLPROC ?fun_ref_92183b49329bb4e4
recv_internal PROC:<{
DROP
DROP
CTOS
TWO
SDSKIPFIRST
1 LDI
1 LDI
LDMSGADDR
OVER
s3 s4 XCHG
s5 s5 XCHG2
4 TUPLE
1 SETGLOB
SWAP
2 SETGLOB
PUSHROOT
CTOS
1 LDI
DROP
<{
NULL
}> PUSHCONT
<{
NULL
}> PUSHCONT
IFELSE
DROP
IFRET
130 THROW
}>
?fun_78250 PROC:<{
PUSHROOT
CTOS
1 LDI
DROP
<{
NULL
}> PUSHCONT
<{
NULL
}> PUSHCONT
IFELSE
?fun_ref_92183b49329bb4e4 INLINECALLDICT
NIP
}>
?fun_ref_92183b49329bb4e4 PROCREF:<{
x{68656C6C6F20776F726C64} PUSHSLICE
}>
}END>c"
`;

exports[`known contracts > should decompile Tact 1.6.0 with other layout and recv_external 1`] = `
""Asm.fif" include
PROGRAM{
-1 DECLMETHOD recv_external
DECLPROC recv_internal
78250 DECLMETHOD ?fun_78250
DECLPROC ?fun_ref_92183b49329bb4e4
recv_external PROC:<{
DROP
DROP
PUSHROOT
CTOS
1 LDI
DROP
<{
NULL
}> PUSHCONT
<{
NULL
}> PUSHCONT
IFELSE
1 GETGLOB
4 UNTUPLE
s2 s3 XCHG
3 BLKDROP
41351 PUSHINT
MYADDR
ROT
SDEQ
THROWANYIFNOT
DROP
NEWC
-1 PUSHINT
SWAP
1 STI
ENDC
POPROOT
}>
recv_internal PROC:<{
DROP
DROP
CTOS
TWO
SDSKIPFIRST
1 LDI
1 LDI
LDMSGADDR
OVER
s3 s4 XCHG
s5 s5 XCHG2
4 TUPLE
1 SETGLOB
SWAP
2 SETGLOB
PUSHROOT
CTOS
1 LDI
DROP
<{
NULL
}> PUSHCONT
<{
NULL
}> PUSHCONT
IFELSE
DROP
IFRET
130 THROW
}>
?fun_78250 PROC:<{
PUSHROOT
CTOS
1 LDI
DROP
<{
NULL
}> PUSHCONT
<{
NULL
}> PUSHCONT
IFELSE
?fun_ref_92183b49329bb4e4 INLINECALLDICT
NIP
}>
?fun_ref_92183b49329bb4e4 PROCREF:<{
x{68656C6C6F20776F726C64} PUSHSLICE
}>
}END>c"
`;

exports[`known contracts > should decompile echo 1`] = `
""Asm.fif" include
PROGRAM{
Expand Down
Loading