Skip to content

feat: dockerize#691

Merged
Jon-Becker merged 2 commits into
mainfrom
jon-becker/dockerize
Apr 30, 2026
Merged

feat: dockerize#691
Jon-Becker merged 2 commits into
mainfrom
jon-becker/dockerize

Conversation

@Jon-Becker

@Jon-Becker Jon-Becker commented Apr 30, 2026

Copy link
Copy Markdown
Owner

What changed? Why?

Added Docker support to make heimdall easier to deploy and distribute. This includes:

  • Dockerfile: Multi-stage build that compiles the Rust binary in a builder stage and packages it in a minimal debian:bookworm-slim runtime image
  • .dockerignore: Excludes unnecessary files from the build context (target, output, .github, .git, markdown files except README)
  • GitHub Actions workflow (.github/workflows/docker.yml): Automates building and pushing Docker images to GHCR with support for both amd64 and arm64 platforms. Images are tagged with:
    • semantic version tags on release
    • latest tag on version tags
    • nightly tag on main branch pushes
    • commit sha tags for traceability

The workflow uses GitHub's build cache and buildx for efficient multi-platform builds.

Notes to reviewers

Key files:

  • Dockerfile (19 lines)
  • .github/workflows/docker.yml (60 lines)
  • .dockerignore (6 lines)

How has it been tested?

Tested via GitHub Actions workflow. The workflow:

  • Runs on pushes to main and all tags
  • Can be triggered manually via workflow_dispatch
  • Builds and pushes to GHCR for both linux/amd64 and linux/arm64

@github-actions

Copy link
Copy Markdown
Contributor

❌ Eval Report for 43228a0

Test Case CFG Decompilation
TransientStorage 100 5
NestedMapping 100 3
NestedLoop 45 5
NestedMappings 70 20
Mapping 100 35
Events 100 45
WhileLoop 65 5
SimpleLoop 70 5
SimpleStorage 100 45
WETH9 85 38
Average 83 20
⚠️ 10 eval(s) scoring <70%

TransientStorage (CFG: 100, Decompilation: 5)

Decompilation

{
  "score": 5,
  "summary": "Decompilation almost entirely fails. Five of six functions are missing — incrementCounter, lock, unlock, getCounter, and isLocked are rendered as nonsensical constant declarations rather than functions, losing all transient storage reads, the counter increment, and the lock toggle logic. Only setTempOwner is present as a callable function, but it is also incorrect: the write uses a bitwise-OR mask instead of a plain assignment, the address-equality require is a tautology, and the function is incorrectly marked pure despite writing transient storage.",
  "differences": [
    "incrementCounter is missing entirely; its counter read, increment, and transient write are not represented",
    "lock() is missing; the transient write setting locked=true is not represented",
    "unlock() is missing; the transient write setting locked=false is not represented",
    "getCounter() is missing as a function; replaced by a constant declaration 'uint256 public constant getCounter = 1' which always returns 1 regardless of transient storage state",
    "isLocked() is missing as a function; replaced by an invalid constant declaration",
    "setTempOwner writes '(address(arg0) * 0x01) | (uint96(transient[0x01]))' instead of a plain assignment, incorrectly ORing with the existing slot value",
    "setTempOwner is marked pure despite performing a transient storage write"
  ]
}

NestedMapping (CFG: 100, Decompilation: 3)

Decompilation

{
  "score": 3,
  "summary": "Decompilation completely fails to capture any functional logic. The decompiler identified 7 function entry points matching the original count (4 explicit functions + 3 public mapping auto-getters), but every function body is replaced with a trivial no-op input validation stub. No storage reads, no storage writes, and no return values are present anywhere in the output.",
  "differences": [
    "setAllowance: no storage write to allowances[owner][spender]; function body replaced with a no-op require",
    "setGrid: no storage write to grid[x][y]; function body replaced with a no-op require",
    "setDeepNested: no storage write to deepNested[a][b][c]; function body replaced with a no-op require",
    "getAllowance: no storage read from allowances[owner][spender]; returns nothing instead of uint256",
    "allowances public getter: no storage read; returns nothing",
    "grid public getter: no storage read; returns nothing",
    "deepNested public getter: no storage read; returns nothing",
    "All 7 functions incorrectly marked pure despite every original function reading or writing storage"
  ]
}

NestedLoop (CFG: 45, Decompilation: 5)

Decompilation

{
  "score": 5,
  "summary": "Decompilation fails to capture the nested loop structure or state mutation. The core logic—iterating loops*loops times and incrementing storage—is entirely absent. The function is incorrectly marked view, and the loop body is replaced with nonsensical require statements that do not approximate the original behavior.",
  "differences": [
    "Function marked as 'view' but original modifies state variable 'number'",
    "Nested double-loop structure (outer i and inner j iterating 0..loops) is completely absent",
    "Core operation 'number += 1' (executed loops*loops times) is never performed",
    "Loop logic replaced with tautological and type-invalid require statements instead of iteration",
    "require(!0 < arg0) uses boolean NOT on integer literal producing type-invalid comparison",
    "require(!number > (number + 0x01)) is type-invalid and does not represent an increment",
    "Overall program behavior is fundamentally different: original accumulates loops^2 increments to storage; decompiled either no-ops or produces invalid Solidity"
  ]
}

CFG

{
  "score": 45,
  "summary": "The CFG captures loop entry points and the outer loop exit, but is missing all loop back-edges and the inner loop exit path. The iterative structure of both loops cannot be observed from the graph.",
  "missing_paths": [
    "Inner loop back-edge: after body execution (number += 1, SSTORE), increment j and jump back to inner loop condition at 0x84 — this edge is entirely absent",
    "Inner loop exit branch: node 8 has a conditional jump (PUSH2 0xb1 JUMPI) but the edge representing j >= loops → 0xb1 is missing",
    "Inner loop exit node (around 0xb1): the basic block that exits the inner loop, increments i, and jumps back to the outer loop header is not present as a node",
    "Outer loop back-edge: after the inner loop completes, increment i and jump back to outer loop condition at 0x77 — this edge is entirely absent"
  ],
  "extra_paths": [
    "Overflow check revert at node 10 (0x01a4-0x017e): compiler-added arithmetic overflow guard for number += 1",
    "CALLVALUE check at node 0→1: compiler-added payable guard",
    "Calldatasize check at node 2→15: compiler-added short calldata revert",
    "Calldata validation revert at node 4→12 and node 5→6: compiler-added ABI decode guards"
  ],
  "observations": [
    "The CFG correctly shows two-level loop entry: outer condition (node 7) feeds into inner condition (node 8) which feeds into the loop body (node 9)",
    "The outer loop exit edge (7→11) is present, confirming the outer loop's termination path is captured",
    "The inner loop body (SLOAD + ADD for number += 1) is present in node 9, but the SSTORE completing the assignment appears to be in a missing continuation block after the overflow check",
    "Approximately half of the loop control flow edges are missing — both back-edges and the inner-loop-exit-to-outer-increment path — making the loops appear as one-shot conditionals rather than iterating structures",
    "Function dispatch for both 'loop(uint256)' and the auto-generated 'number()' getter are fully represented"
  ]
}

NestedMappings (CFG: 70, Decompilation: 20)

Decompilation

{
  "score": 20,
  "summary": "Decompilation fails to capture the nested mapping structure and loses critical logic in all three functions. The approve function drops the msg.sender outer key, the allowance function loses its second argument and return value entirely, and the public getter is unresolved with no functional body.",
  "differences": [
    "allowance(address,address) decompiled as a single-argument function missing the second address (spender) parameter entirely",
    "allowance function returns no value; the original returns allowances[owner][spender]",
    "approve stores storage_map_a[arg0] = arg1 instead of the nested allowances[msg.sender][arg0] = arg1, dropping msg.sender as the outer mapping key",
    "The auto-generated public getter for the allowances mapping (Unresolved_55b6ed5c) has no storage read and no return value",
    "approve is incorrectly marked payable, changing its ETH-acceptance behavior from the original"
  ]
}

Mapping (CFG: 100, Decompilation: 35)

Decompilation

{
  "score": 35,
  "summary": "The decompiler identified only 2 storage maps instead of the 3 distinct mappings in the original. All three setter functions (setBalance, setOwner, register) incorrectly write to the same storage_map_a, while the corresponding auto-generated getters (balances, owners, registered) read from storage_map_b — meaning writes and reads target different storage slots, making all three mappings non-functional. Individual operation logic (bool packing, address masking) is partially preserved but the fundamental storage routing is broken.",
  "differences": [
    "All three setter functions (setBalance, setOwner, register) write to storage_map_a when they should target three distinct storage slots; this causes all three mappings to overwrite each other",
    "The balances auto-getter reads from storage_map_b while setBalance writes to storage_map_a, so the getter never reflects written values",
    "The owners auto-getter reads from storage_map_b while setOwner writes to storage_map_a, so the getter never reflects written values",
    "The registered auto-getter reads from storage_map_b while register writes to storage_map_a, so the getter never reflects written values",
    "setOwner uses a bitwise OR with the existing slot value (| uint96(storage_map_a[var_a])) instead of a clean assignment, which would corrupt the stored address if upper 96 bits were previously non-zero"
  ]
}

Events (CFG: 100, Decompilation: 45)

Decompilation

{
  "score": 45,
  "summary": "5 of 7 functions are at least partially preserved, but emitTransfer and emitApproval are completely absent — their decompiled counterparts (0x5687f2b8 and 0x23de6651) have incorrect signatures (single address param instead of two addresses + uint256) and emit no events at all. The Approval event is not declared. emitLog and emitLogBytes are present but use uint256 parameters and raw memory offset notation instead of string/bytes, making the calldata-forwarding logic ambiguous.",
  "differences": [
    "emitTransfer(address,address,uint256) is not decompiled: its apparent counterpart (Unresolved_5687f2b8) takes only one address parameter and emits no Transfer event",
    "emitApproval(address,address,uint256) is not decompiled: its apparent counterpart (Unresolved_23de6651) takes only one address parameter and emits no Approval event",
    "The Approval event is entirely absent from the decompiled contract's event declarations",
    "emitLog and emitLogBytes are decompiled with uint256 parameter types instead of string/bytes; the event payload is passed via raw memory offset expressions ((var_b + 0x20) - var_b) making the calldata forwarding logic unclear",
    "emitMultiple emits Transfer(0, arg0, arg1) using integer literal 0 instead of address(0) for the from field, though this is semantically equivalent"
  ]
}

WhileLoop (CFG: 65, Decompilation: 5)

Decompilation

{
  "score": 5,
  "summary": "Decompilation catastrophically fails to reproduce the loop logic. The while loop is entirely absent, no state mutation occurs, and the function is incorrectly marked `view`. The output contains three nonsensical `require` statements that bear no relation to the original logic.",
  "differences": [
    "While loop is completely absent — the iterative control flow (i=0; while i<loops) is not represented at all",
    "State mutation `number = number + 1` executed on each iteration is missing entirely",
    "Function incorrectly marked `view` when it modifies state variable `number`",
    "Loop counter logic (`i = i + 1`, termination condition `i < loops`) is not present",
    "Three spurious `require` statements introduced that have no equivalent in the original (including tautology `arg0 == arg0` and nonsensical `!number > (number + 0x01)`)"
  ]
}

CFG

{
  "score": 65,
  "summary": "The CFG captures the while loop's condition check and both branch directions (body vs. exit), plus function dispatch and the number() getter, but critically omits the loop back-edge and the second half of the loop body (SSTORE for number, i = i + 1, and the return jump to the loop header). Without the back-edge, the loop cannot be distinguished from a one-shot if-statement in the graph.",
  "missing_paths": [
    "Loop back-edge: after the loop body executes, there is no path returning to the loop condition check at 0x77 (JUMPDEST). Node 8 ends at the overflow JUMPI (0x018a) and no successor node contains the code that jumps back to the loop header.",
    "Second half of loop body: the continuation after the first overflow check passes (0x0193+) is entirely absent — this includes the SSTORE writing the incremented number back to storage, the i = i + 1 addition with its own overflow check, and the JUMP instruction that closes the loop."
  ],
  "extra_paths": [
    "CALLVALUE check at contract entry (node 0 -> node 1 REVERT): compiler-added non-payable guard for the constructor.",
    "Overflow/underflow panic path (node 8 -> node 9): compiler-inserted arithmetic safety check for Solidity 0.8+ checked arithmetic on number + 1.",
    "CALLDATASIZE < 4 revert (node 2 -> node 14): compiler-added ABI dispatch guard.",
    "Input type validation revert (nodes 4 -> 11, 5 -> 6): compiler-added calldata bounds and type-check reverts.",
    "Inline ABI codec and type-normalisation library stubs embedded within nodes 4, 5, 7, 8, and 13."
  ],
  "observations": [
    "Node 7 correctly merges loop initialisation (i = 0 at 0x73-0x76) with the loop header (0x77 JUMPDEST) and condition check (LT/ISZERO/JUMPI at 0x7a-0x7f), and the two outgoing edges (7->8 body, 7->10 exit) are both present.",
    "The number() public getter (nodes 12 -> 13) is fully represented including SLOAD and ABI-encoded RETURN.",
    "The CFG tool appears to have stopped emitting nodes after the first inlined overflow-check helper; all bytecode from 0x0193 onward is absent, cutting the loop body in half and losing the back-edge.",
    "Because the back-edge is missing the graph has no cycle, making the while loop structurally indistinguishable from a conditional branch in this CFG representation."
  ]
}

SimpleLoop (CFG: 70, Decompilation: 5)

Decompilation

{
  "score": 5,
  "summary": "Decompilation fails to capture the core logic. The for-loop structure is entirely absent, the state mutation (number++) is missing, and the function is incorrectly marked as view. The emitted require statements are nonsensical tautologies or malformed expressions that bear no meaningful relationship to the original control flow or state changes.",
  "differences": [
    "The for-loop is completely absent; no iteration logic is represented",
    "The state mutation (number++) is missing; the storage variable 'number' is never written",
    "Function mutability is 'view' instead of state-mutating, which is functionally incorrect",
    "require(arg0 == arg0) is an unconditional tautology with no equivalent in the original",
    "require(!0 < arg0) incorrectly implies a minimum input constraint of arg0 > 1 that does not exist in the original",
    "require(number - 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) misrepresents the overflow-increment as a revert condition rather than an assignment"
  ]
}

SimpleStorage (CFG: 100, Decompilation: 45)

Decompilation

{
  "score": 45,
  "summary": "initialize() is correctly decompiled as packed-slot bit manipulation. setValue() is functionally correct aside from a spurious tautological require. However, setOwner() incorrectly ORs the new address with the lower 96 bits of the old slot, producing wrong owner values when the old address had set bits in positions 0-95 that the new address lacks. reset() similarly fails to fully zero the owner: the two-step bit masking leaves the lower 96 bits of the original address intact. The initialized public state variable and its getter are entirely absent from the decompiled output.",
  "differences": [
    "setOwner() uses `owner = address(arg0) | uint96(owner)` which ORs the new address with the lower 96 bits of the old owner, corrupting the result whenever old and new addresses differ in the lower 12 bytes",
    "reset() does not fully zero the owner: the masking sequence leaves bits 0-95 of the original owner address intact rather than setting owner to address(0)",
    "The initialized public state variable and its auto-generated getter function are missing entirely from the decompiled contract"
  ]
}

WETH9 (CFG: 85, Decompilation: 38)

Decompilation

{
  "score": 38,
  "summary": "Deposit, withdraw, and totalSupply logic are largely preserved, but the storage layout is confused: the public balanceOf getter reads from a different slot than deposit/withdraw write to. The allowance nested mapping loses the owner dimension entirely, breaking approve, allowance reads, and transferFrom. The allowance-exemption condition in transferFrom and transfer is inverted (checks src == msg.sender instead of src != msg.sender), and both functions contain large blocks of dead code after unconditional return statements.",
  "differences": [
    "balanceOf public getter reads from storage_map_d while deposit/withdraw use storage_map_c — the getter returns the wrong storage slot",
    "allowance mapping loses the owner (arg0) dimension: returns storage_map_d[arg1] instead of allowance[arg0][arg1], making all allowance reads incorrect",
    "approve writes to storage_map_c[arg0] (single-key, wrong map) instead of the nested allowance[msg.sender][arg0] mapping — allowance is stored in the wrong location",
    "transferFrom allowance-exemption check is inverted: checks arg0 == msg.sender (always skips deduction when caller is src) instead of arg0 != msg.sender (skip only when src IS the caller)",
    "transfer contains a tautological require(msg.sender == msg.sender) instead of a meaningful guard, and the allowance-exemption branch is incorrectly structured",
    "transfer and transferFrom both contain unreachable code blocks after unconditional return statements, meaning the correct code paths are never executed in several branches",
    "approve uses storage_map_c for allowance writes while withdraw/deposit also use storage_map_c for balance operations — balance and allowance share the same mapping, causing storage collisions"
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Coverage Report for 43228a0

Metric Value
Base branch 66.04%
PR branch 65.93%
Diff -0.12%

@Jon-Becker Jon-Becker merged commit a981d48 into main Apr 30, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant