vodka / The protection engine

What the engine does. And where it stops.

A plain account of how vodka protects your Lua and Luau code: the near-native default pipeline, the opt-in Maximum protection mode, the per-key leak-tracing fingerprint, and the honest limits of anything that runs on a client.

The short version

Your script is rewritten into something hard to read and copy, and it still runs. The default pipeline is a pure source rewrite that stays near native, so most scripts need nothing else. When the logic is worth hiding, an opt-in Maximum protection mode adds a per-function bytecode VM. None of it makes a client a vault, and this page says so plainly. See the proof on the benchmarks, and what changed in each release on the changelog.

Applied by default

The default pipeline, near native.

Every build runs the same set of source transformations. They change how the program is written without changing what it does, so the protected script runs within noise of the original. There is no bytecode VM in the default path.

01

Scope-aware renaming

Local and upvalue names are rewritten with scope awareness, so meaningful names do not survive into the output. Globals, table fields, method names, string keys and type names are never renamed, so behavior is preserved.

02

Encoded strings

String constants are byte-encoded, and their decode key is derived at runtime rather than shipped in plaintext beside the ciphertext. Reading the file does not recover the strings; running the loader does.

03

Encoded numbers

Integer literals are rewritten as reversible arithmetic and floats are exact-rewritten, so literal values are not sitting in plain text. A constant-folder reverses the arithmetic, which the benchmarks state plainly.

04

Control-flow flattening

Supported functions are rearranged into dispatch state-machines, so the logic is harder to read top to bottom, and decoy functions are injected alongside the real ones.

05

Minify and watermark

The output is compacted and carries a per-build watermark. The watermark is a build identifier, not a claim of tamper resistance.

These are source rewrites, so default protection lands within measurement noise of the unprotected script. See the measured overhead on the benchmarks.

Opt-in, per script

Maximum protection, selective by default.

A selective per-function bytecode VM.

Turn Maximum protection on per script and supported functions are compiled to bytecode and interpreted by a generated per-function virtual machine, on top of the default transformations. Its operator handlers are table-indirected rather than readable one-line cases, so the interpreter does not read as a plain list of operations.

The VM is selective. It virtualizes only cold functions and keeps hot or looping code (a function with a loop, or one called from inside a loop) native automatically, so Maximum stays near-native on typical code instead of paying the per-function VM cost everywhere. Functions outside the supported set also keep native execution, so behavior is preserved rather than guessed. Per-function directives let you override the choice: --@protect forces a function into the VM, and --@novm or --!native keep one native.

Self-integrity anti-tamper

The virtualized bytecode can carry a self-integrity check, so editing its own code breaks the chunk instead of running tampered.

Because the VM is selective, Maximum stays near-native on typical code (around 1.17x on the published fixture, where the hot function stays native). The heavy per-function VM cost only lands on a function you force-virtualize onto a hot path with --@protect; on the published fixture that one forced hot function is roughly 113x. Measure your hot path before forcing the VM onto it.

Per-key leak tracing

A fingerprint that survives a beautify.

Each keyed delivery carries a traceable fingerprint, so a casually leaked copy maps back to the key that received it. The mark is designed to survive minification and a StyLua beautify, so re-indenting the file does not remove it. No-key and premium deliveries are unstamped.

This traces lazy re-sharing, not a determined leaker. An attacker who re-obfuscates or rebuilds the chunk strips the fingerprint, and two buyers who diff their copies can find and remove it. We would rather state that than imply the mark is unbeatable.

Where protection ends

Honest about the boundary.

No client-side engine can make a delivered secret unrecoverable. An authorized client receives the code it runs, so running the loader derives the decode key and dumps the entire string pool in plaintext, and a minimal constant-folder reverses the integer arithmetic to nothing. This is true of every obfuscator, vodka included.

What the engine does is raise the cost of casual inspection and copying. What actually gates access is server-side: the key check, expiry, revocation and HWID locks are enforced where the client never sees how the decision was made. Keep anything that must stay secret on a server, and read the benchmarks for exactly what the engine does and does not defeat.

Keep exploring

Where to go next.

Questions

A few things worth knowing.

01Is the default protection slower than my unprotected script?

Barely. The default pipeline is a pure source rewrite (renaming, encoded strings and numbers, control-flow flattening, a watermark), so it runs within noise of native Luau, roughly 1.14x on the published fixture. The per-function bytecode VM is not in the default path. Measure your own hot path with the Roblox MicroProfiler or an os.clock() A/B loop before you ship.

02What does Maximum protection add, and when should I use it?

It adds the per-function bytecode VM with table-indirected operator handlers and an opt-in self-integrity anti-tamper check, on top of the default transformations. The VM is selective: it virtualizes only cold functions and keeps hot or looping code native automatically, so Maximum stays near-native on typical code (around 1.17x on the published fixture, where the hot function stays native). The heavy per-function VM cost only lands if you force the VM onto a hot path with the --@protect directive; --@novm and --!native keep a function native. Turn Maximum on per script when the logic is worth hiding; the engine keeps your frame-time hot path native unless you force it otherwise.

03Can the engine keep a secret safe on the client?

No, and we will not claim otherwise. Running the loader derives the decode key and recovers the whole string pool in plaintext, and a constant-folder reverses the integer arithmetic. Any value shipped to an authorized client is recoverable. The engine raises the cost of casual inspection and copying; it does not make a client a vault. Keep secrets and access decisions on the server.

04How reliable is the per-key leak-tracing fingerprint?

It traces casual re-sharing. Each keyed delivery carries a traceable fingerprint that maps a leaked copy back to the key that received it, and the mark now survives minification and a StyLua beautify. A determined attacker who re-obfuscates or rebuilds the chunk strips it, and two buyers who diff their copies can find and remove it. No-key and premium deliveries are unstamped.

Keep building

Protect your next build.

Default protection is near native and on by every build. Bring your source and see for yourself.

Get started
vodka / Product notesUpdated