A summary of SimilarityDB's distinctive design decisions and features, collected from the
repository (README, docs/, sql/, and the Rust crates featurizer/, engine/,
frontend/, watermark/). Each section points to the source for details.
| What it is | An academic, non-commercial platform for binary code similarity, lineage and clustering. It covers PE, ELF and Mach-O on x86, x86-64, ARM32, ARM64 and RISC-V |
| Analysis core | IDA Pro 9.5 (idalib) + Hex-Rays, run once per sample |
| Permanent record | A versioned, content-addressed IR (<sha256>.ir.jsonl.gz). Binaries can be deleted after analysis |
| Storage | ClickHouse, with a content / references split for every code table |
| Compute | Three static, dependency-pinned Rust binaries: featurizer, similarity engine, web UI |
| Philosophy | Deterministic and explainable. No ML, no LLMs, no deobfuscation |
| Scale target | 10⁹ binaries, about 10¹² function occurrences, about 10¹⁰ distinct functions |
sample ─► start.py ─► format extractors (PE/ELF/Mach-O, hashes, YARA, CAPA, DIE, malcontent, IOCs)
│ └─► similaritydb_* tables
└─ --decompile ─► IDA 9.5 + Hex-Rays (isolated child process)
└─► IR (archived, the permanent record)
└─► similaritydb-featurize (Rust) ─► code_ida_* (content / references)
└─► similaritydb-engine (Rust) ─► similarity_* tables
└─► similaritydb-web (Rust, no JS)
| Choice | Why (as documented) |
|---|---|
| ClickHouse | Columnar storage with ZSTD codecs. ReplacingMergeTree makes re-inserting identical content free and lets reprocessed data replace old rows. Skip indexes (bloom_filter, tokenbf_v1, ngrambf_v1, minmax, set) support hash, name and string search. Null engine + materialized views fold rows on ingest. AggregatingMergeTree with uniqState keeps prevalence counts |
| IDA Pro 9.5 + Hex-Rays (idalib) | Exports native disassembly, CFG, microcode at MMAT_PREOPTIMIZED (stored as llil) and MMAT_LVARS (stored as mlil), and pseudocode / ctree (stored as decompilation) |
| Versioned IR as the source of truth | Only the IDA stage needs a license. Every feature can be recomputed from archived IR in seconds per sample, with no IDA, when an algorithm changes (scripts/reingest_from_ir.py, bench/refeaturize.py) |
| Rust featurizer | About 100× faster than the Python reference, with output byte-identical to it (parity checked by scripts/featurizer_parity.py, 100% on 17 samples). SHA-256, MurmurHash3 and TLSH are implemented in the crate, with SHA/AVX2 fast paths detected at run time. Measured cost: 1.42 ms CPU per function |
| Dependency-free Rust engine | No crates at all: std::thread::scope, plain HTTP to ClickHouse (RowBinary in, TabSeparated out), hand-written JSON, and a custom thread-caching allocator, because musl's malloc serialized the matching threads |
| Static musl binaries | Pinned toolchain (Rust 1.99.1), committed Cargo.lock, linked with rust-lld. The Dockerfile builds them in a first stage, and IDA is mounted from the host at runtime |
| Python ingestion | Modular extractors, spawn multiprocessing, swap monitoring and pool restarts, and S3/MinIO, date-range, catalog and Nomad job modes |
| Deterministic magic-byte file typing | Replaced Magika, which is an ML classifier, to keep the pipeline ML-free (similaritydb/filetype.py) |
This is the central storage idea (docs/TOOLING.md, sql/code_ida_schema.sql).
Every code table family is split in two:
| kind | key | holds | rows |
|---|---|---|---|
*_content |
hash of all stored content columns | function text, metrics, features | once per distinct function, across all samples |
*_references |
(sample sha256, function address) | address, name, flags, links to content hashes | once per function per sample |
A library routine found in 10,000 samples is stored once in each content table and 10,000 times as a small reference row.
Families: decompiled (pseudocode), disassembled, low-level microcode, function features (v2),
CFG, plus per-sample code_ida_sample_stats, code_ida_call_edges and
code_ida_strings_by_binary.
Rule: a content key must hash every column of its row, so different functions can never overwrite each other. Anything that depends on addresses or on the sample (names, flags, callee addresses, string offsets) belongs on the references side.
Because content and references are separate, "how common is this code?" is cheap to answer:
count() AS refs, uniqExact(sha256) AS samples, uniqExact(address) AS addrs,
uniqExact(name) AS names for a disassembled_function_hash. It then lists every sample
and address where the same function body appears, with the current sample first
(frontend/src/functions.rs).code_ida_pseudocode_line_stats is an AggregatingMergeTree
fed through a Null table and a materialized view. For each normalized pseudocode line it
keeps uniqState of the functions and samples it appears in, so re-ingestion is
idempotent. The UI shades lines by heat ("seen in 24 of 24 samples (100 functions)") and
has a common lines page (frontend/src/explore.rs).frontend/src/refs.rs).code_ida_sample_stats records functions_total, functions_user, functions_library,
functions_thunk, functions_external, functions_small, functions_decompiled,
functions_failed, and name sources (names_symbol, names_flirt, names_goresym,
names_dummy). It also records code coverage (exec_bytes, user_function_bytes,
orphan_code_bytes, coverage_ratio), arch and platform, the packed flag, and every tool
version (IDA, Hex-Rays, IR, featurizer).
It is written last and acts as the commit marker: a sample counts as analysed only
when its newest row has status ok, so a half-finished export is retried. Re-ingesting a
sample first deletes its old per-sample rows. Shared content rows are kept.
Every IDA function is exported with flags: type (USER / LIBRARY / THUNK / EXTERNAL /
UNKNOWN), small, thunk, library (FLIRT, lib_verified), and name source. Nothing is dropped
at extraction. Filtering decisions belong to the consumer. For example, the engine
down-weights library code in lineage mode, but still reports library_matches.
sub_/loc_ → ADDR), skeleton hashes, and pseudocode template hashes. Disassembly
normalization has three levels: fully normalized, API-normalized and category-normalized
(docs/CODE_ANALYSIS_APPROACH.md).docs/pseudocode-analyses.md): Deckard-style AST
vectors, statement winnowing, api_args (call argument shapes), types/structs, an idiom
library (memcpy loops, XOR loops, CRC/base64 tables…), control dependence, dataflow
(BSim/Esh-style strands), and value-set analysis. dataflow + api_args are on by default
because they were chosen on the benchmark.docs/featurizer-v2.md §8). Arch-specific
features are only compared within one (processor, bits).TLSH (Trend Micro Locality Sensitive Hash) is a fuzzy digest built from bucketed counts of byte triplets in a sliding window. It comes with a distance score.
Implementation. featurizer/src/tlsh.rs is a pure-Rust port of the Apache-2.0 reference
implementation, matching the py-tlsh 4.7.2 tlsh.hash() configuration: 128 buckets, 1-byte
checksum, "T1" prefix, at least 50 bytes of input, non-conservative. Its output is
byte-identical to the Python pipeline. The license note is in featurizer/NOTICE.
Where it is stored. Each digest is a Nullable(FixedString(72)) column:
| column | input | table(s) |
|---|---|---|
tlsh_disassembly |
native disassembly text | disassembled references, similarity metrics, llil references |
tlsh_llil / tlsh_llil_new |
low-level microcode (MMAT_PREOPTIMIZED) |
similarity metrics, llil references (bloom-filter indexed) |
tlsh_instruction_typed_llil |
typed low-level microcode | similarity metrics |
tlsh_mlil_skeleton, tlsh_mlil_typed |
MMAT_LVARS microcode (skeleton / typed) |
similarity metrics |
cfg_feature_tlsh |
per-block CFG feature vectors in BFS order | code_ida_cfg_functions |
The web UI shows all of them under "fuzzy hashes" on the function page
(frontend/src/functions.rs).
Why it is not a matcher.
- In the clustering prototype's ablation (docs/similarity-clustering-draft.md), a TLSH
tier (tlsh_llil distance ≤ 30) carried 64% of inter-family matched weight but only
5% of intra-family weight. It raised the noise floor between unrelated packages from
0.005 to 0.043 (max 0.103), so it was switched off.
- Function-level token strings are short and low-entropy, which is the worst case for this
kind of digest. This agrees with Pagani et al. (2018) (docs/literature-review.md §3.4).
- TLSH has no index: at a scale of 10⁹ samples it could only check candidate pairs that
another tier already proposed, never generate them. The draft rejects adding more TLSH
variants for this reason.
- On the benchmark it is weak across architectures, for example tlsh_llil P@1 is 0.05 on
the arch axis (bench/RESULTS.md). TLSH of disassembly is classed as arch-specific
(docs/featurizer-v2.md §8).
- The feature critique notes the redundancy: TLSH appears in three tables, and
tlsh_llil_new duplicates tlsh_llil.
The literature review leaves one question open: whether TLSH could break ties for large functions only, where its statistics mean something.
The idea that recurs across the whole design is to describe the same function at several levels of abstraction. Each level has its own hashes, and the engine prefers the most concrete level that still matches. A lower level is more precise; a higher level survives more change (compiler, flags, architecture, platform).
| level | stored as | survives | arch class |
|---|---|---|---|
| native disassembly + CFG | disassembly, cfg |
nothing beyond relinking | arch-specific |
Hex-Rays microcode MMAT_PREOPTIMIZED |
llil |
relocation, renaming | measured not neutral (same arch only) |
Hex-Rays microcode MMAT_LVARS |
mlil |
ISA change (P@1 0.65–0.67 across ISAs) | neutral |
| pseudocode / ctree | decompilation |
ISA and platform | neutral |
docs/CODE_ANALYSIS_APPROACH.md):GPR, MEM, CONST, DATA_REF.GPR_DATA, MEM_STACK, CONST_LARGE, API_CreateFileW.REG_32, MEM_BASE_OFFSET, CONST_HEX, API_FILE_OP.<TARGET>.sub_/loc_/…) with ADDR, drops short and collapses blanks. It is computed in SQL by
the engine.P, so 32- and 64-bit
builds hash alike.SDBL<i>_, which gives the
template and template_skeleton_hash. These are also the lookup key for watermark
detection. A normalized pseudocode hash drives the dnorm tier.ctree_control_hash (only the if / loop / switch nesting).__imp_, j_, leading
underscores and stdcall decoration. String skeletons reduce letters to a and digits to
0 and keep format specifiers.Tier order puts concrete before abstract: exact → norm → dnorm → pic → bset →
struct → MinHash tiers → blocks → consts → api → fused → callgraph. The
greedy 1:1 assignment takes the best tier first. Each match records its tier and
quality, and a separate neutral quality that counts only the arch-neutral levels. That
neutral quality is what the cross-architecture scores (xarch_*) are built from.
engine/README.md documents the engine.
exact, norm, dnorm (pseudocode), pic, bset
(block-hash set), struct, mlil MinHash, minhash (llil), blocks, consts, api,
fused, callgraph. Every match is tagged with the tier that produced it.contain_a_in_b,
contain_b_in_a).xarch_*) estimate how much of the similarity would
survive a recompile for another ISA.--df-unit sample | source | cluster. This stops
an attacker or an unbalanced feed from hiding a family by uploading many near-copies.similarity_runs is written last, so a run is visible only when
complete. A partial (--samples) run never replaces full-corpus results.
similarity_latest_* views always point to the newest finished run.similarity_function_matches), with per-tier counts and
weights for each sample pair.similaritydb-webGET forms
only, under a strict CSP./help glossary.sql/web_user.sql), whitelisted parameters, and generic error
pages. The server is bounded: 8 workers, a queue of 64, 16 KiB request head, and per-IP
limits of 10 req/s. The admin area uses PBKDF2-SHA256 (600k rounds) Basic auth with IP
lockout, and refuses plaintext on non-loopback addresses.watermark/ and docs/watermarking.md cover this.
(deployment key, content hash). The same function always renders the same way for one
deployment.tinyjson.docs/FUZZING.md).bench/RESULTS.md), and engine thresholds were chosen from measured precision.scripts/cluster_prototype.py validates the engine.docs/feature-design-critique.md (critique items are
referenced in commit messages) and docs/literature-review.md.docs/performance.md): 10¹² functions ≈ 394,000 core-hours, about
16 days on 1,000 cores. The engine handles 4,000 synthetic samples in about 30 s on 12
threads.Alongside code similarity, per-format extractors fill the similaritydb_* tables:
.yarac), malcontent, IOC extraction, and Go metadata (GoReSym).This roadmap is planned the way a chess engine picks a move. It first evaluates the current position. Then it generates candidate moves, scores each one, and looks several moves ahead for the line that leaves the strongest position. Lines that lose material are pruned.
Every move has to be legal under the project's own rules (docs/literature-review.md §1.2):
Tags used below: - [repo]: the repository already names this as a gap or open question. - [new]: a proposal that does not appear in the repository.
Strengths (material we have): - content / references storage, which gives deduplication and cheap prevalence counts; - the IR archive, so any feature can be recomputed without IDA; - 13 explainable matching tiers, with lineage and reuse modes, containment and cross-architecture scores; - a benchmark that covers optimization level, architecture, platform and stripping; - a hardened UI, watermarking and fuzzing.
Weaknesses (where the opponent attacks):
| weakness | evidence |
|---|---|
| Recall across optimization levels (-O0 against -O2) is the weakest axis | engine recall 0.53 opt vs 0.84 arch vs 0.98 platform (engine/README.md) |
Name leakage: decomp_norm, disasm_norm and pic keep real callee and global names, so a stripped build cannot match its unstripped twin on these tiers |
bench/RESULTS.md §6.5 [repo] |
| Statically linked CRT code makes every Windows build look related | bench/RESULTS.md §6.7 [repo] |
| No compiler axis (clang only; no gcc or MSVC) | bench/RESULTS.md [repo] |
| The engine runs as a batch: all samples, all candidate pairs, every run. Its memory grows to 5.5 GB at 4,000 samples | docs/performance.md |
| LSH has no band table in ClickHouse ("the only big win … is a schema change") | docs/performance.md [repo] |
| The IR is stored per sample, not content-addressed per function | docs/pseudocode-analyses.md "IR storage at scale (to implement)" [repo] |
| Scores are not calibrated: there is no p-value or bit count for "these two samples are related" | literature review §3.10 [repo] |
| Every hash depends on the IDA version | clustering draft, open question 3 [repo] |
Evaluation: the position is strong. The project has excellent foundations and measurement, but three structural problems block the 10⁹ target: the batch engine, the missing band table, and per-sample IR. Precision has two cheap leaks: name leakage and CRT code. Fix the cheap leaks first. Then rebuild the foundation for scale, and only after that add features.
Columns: - Gain: expected value, 1 to 5. - Cost: effort, 1 (days) to 5 (months). - Risk: chance it doesn't pay off, low / mid / high. - Unlocks: the moves that become possible afterwards. In chess terms this is tempo: a move that opens many others is worth more than its own gain.
| # | move | gain | cost | risk | unlocks |
|---|---|---|---|---|---|
| A1 | Name-free normalizers [repo]: replace every non-import callee or global name with a placeholder, and map imports to normalized API names, in decomp_norm, disasm_norm and pic |
4 | 1 | low | stripped ↔ unstripped matching on exact tiers; A2 |
| A2 | CRT alias folding [repo]: fold __mingw_*, __stdio_common_*, glibc __*_chk and Darwin $INODE64 onto their POSIX names |
2 | 1 | low | cross-platform api tier |
| A3 | Toolchain library auto-flagging [repo]: a function present in more than X% of the samples of one (platform, toolchain) is flagged LIB_INFERRED |
4 | 2 | mid | clean Windows lineage; C1 |
| A4 | Information-content weight for exact tiers [repo, open question]: q = 1 becomes q·min(1, log2(N/df)/b₀), so a trivially small dnorm collision stops counting as full evidence |
3 | 1 | low | D1 |
| A5 | Fix watermark bug W1 [repo] | 1 | 1 | low | - |
| # | move | gain | cost | risk | unlocks |
|---|---|---|---|---|---|
| B1 | LSH band table in ClickHouse [repo]: code_ida_lsh_bands(tier, band_id, band_value, features_hash), ordered by (tier, band_id, band_value). Candidate lookup becomes an indexed range read |
5 | 2 | low | B2, E1, E3 |
| B2 | Incremental engine [new]: similaritydb-engine add --samples NEW queries only the new samples against the band table and exact-key indexes, scores only the pairs that involve them, and updates clusters with incremental union-find. A full batch run becomes a rare, periodic rebuild |
5 | 4 | mid | near-real-time clustering; E2, E4 |
| B3 | Content-addressed IR store [repo, designed]: ir/fn/<hh>/<hash>.json.zst plus per-sample manifests. Re-featurization runs once per distinct function, and storage shrinks by the dedup ratio |
4 | 3 | low | cheap featurizer upgrades; B4 |
| B4 | Featurizer content cache [repo, mentioned in perf]: skip featurizing any function whose content hash already has a row at the current featurizer version | 4 | 2 | low | well under the projected 394k core-hours |
| B5 | Per-IDA-version keys [repo, open question]: store each content key alongside (ida_version, hexrays_version), plus a differential study across IDA releases |
3 | 2 | mid | safe IDA upgrades |
| B6 | Run retention and TTL [repo]: old engine runs are never deleted today | 1 | 1 | low | - |
| # | move | gain | cost | risk | unlocks |
|---|---|---|---|---|---|
| C1 | Stronger MMAT_LVARS normalization for -O0 [repo]: remove stack spills and reloads and copy-only temporaries, and canonicalize address arithmetic | 4 | 3 | mid | opt-axis recall; benefits every neutral tier |
| C2 | Logical-function features [repo, P1.2]: inline small callees and thunk chains before hashing (IDA_INLINE_SMALL_HELPERS exists but is opt-in and unmeasured) |
3 | 2 | mid | robustness against function splitting and outlining |
| C3 | Call-graph context key [repo, open question]: an exact tier on hash(key, multiset of callee keys, multiset of caller keys) | 3 | 2 | mid | propagation seeds |
| C4 | v3 rarity tier [repo, planned]: crypto_tags, rare string_skeletons and rare constants as a high-precision, low-recall tier |
3 | 1 | low | family signatures (E3) |
| C5 | String references by text [repo]: string_refs stores only offsets today |
2 | 1 | low | C4 |
| C6 | Deterministic I/O hashing [repo, open question]: run MMAT_LVARS microcode on fixed input vectors and hash the outputs, Pewny-style | 4 | 5 | high | the cross-compiler axis |
| # | move | gain | cost | risk | unlocks |
|---|---|---|---|---|---|
| D1 | Calibrated significance [repo, open question]: express each sample-pair score as bits or an e-value against a null corpus of unrelated pairs. The UI then shows "1 in 10⁹ by chance" rather than "0.31" | 5 | 3 | mid | thresholds that mean the same everywhere; E4 |
| D2 | Compiler axis [repo]: rebuild the benchmark corpus with gcc and MSVC (clang-cl) | 4 | 2 | low | honest numbers; tuning C1 and C6 |
| D3 | Held-out threshold evaluation [repo]: freeze the thresholds, then score a corpus they were not tuned on, reporting P@k, MRR and PR curves | 3 | 1 | low | trustworthy defaults |
| D4 | Adversarial axis [repo, open question]: O-LLVM / Tigress-style transformations (bogus control flow, splitting, substitution). We measure how much we degrade; we don't deobfuscate | 3 | 2 | low | known limits |
| D5 | Ground-truth capture in the UI [new]: an admin-only "confirm / reject" on function and sample matches, stored as labels. These are used only for evaluation, never for training | 3 | 2 | low | D3 on real data |
| # | move | gain | cost | risk | unlocks |
|---|---|---|---|---|---|
| E1 | Two-sample diff view [new, data exists]: a BinDiff-style page with matched, unmatched and changed functions, side-by-side pseudocode, and statement-level diffs from the statements analysis hash lists |
5 | 2 | low | patch and variant analysis |
| E2 | Cluster history [new]: track cluster ids across engine runs as birth, growth, merge, split and death events, with a timeline per cluster | 4 | 2 | low | E5 |
| E3 | Feature-level, arch-neutral family signatures [repo, open question]: from a cluster's stable core (functions in every member), emit a rule over block hashes, API-argument tags and rare constants. Ship a matcher (similaritydb-sig scan), and also emit YARA where byte patterns exist. Evaluate with leave-one-out |
5 | 4 | mid | detection, not only clustering |
| E4 | Directed lineage / phylogeny [repo, open question]: orient edges with containment asymmetry, compile timestamps and first-seen dates, then build a minimum-cost arborescence per cluster ("v2.1 → v2.2 added module X") | 4 | 3 | high | family evolution reports |
| E5 | Watchlists [new]: "notify when a new sample joins cluster C or contains function F". Results are derived from the latest runs, the same way analysis jobs are derived today | 3 | 2 | low | operational use (needs B2) |
| E6 | Module recovery [repo, open question]: group functions into modules (BCD-style: call graph, data refs, address locality), then compute module-level df and containment ("contains zlib inflate 1.2.x") | 4 | 4 | high | readable explanations; better library suppression |
| E7 | Pairwise explanation page [new, data exists]: a deterministic narrative of the per-tier counts and weights stored for each pair, plus the top contributing functions and why each counted | 3 | 1 | low | trust in scores; D5 |
| E8 | Read-only JSON API [new]: the same parameterized queries as the UI, behind the same limits and the read-only ClickHouse user | 3 | 2 | low | integrations (MISP / STIX export of clusters) |
| E9 | Metadata similarity channel [repo, design rule]: imphash, Rich header, telfhash and Authenticode signer as a separate, labelled relation, never mixed into code scores | 2 | 1 | low | pivoting in the UI |
| E10 | Query by snippet (CLI) [new]: similaritydb-query --bytes FILE --arch x86-64 lifts a code fragment with the same IDA/IR pipeline and runs it against the band table |
3 | 3 | mid | hunting from a report or a memory dump |
The search goes about 8 moves deep. Order matters: each move opens the next.
1. A1 name-free normalizers ── forcing: stops a precision leak
2. A3 toolchain library flagging ── forcing: unblocks Windows lineage
(A2, A4, A5 in the same sprint: one-day moves)
3. D2 gcc + MSVC benchmark ── measure before the big work
4. B1 LSH band table ── the foundation every later move needs
5. B3+B4 content-addressed IR + cache ── makes featurizer upgrades cheap
6. B2 incremental engine ── from batch to online; the key move
7. E1 diff view + E7 explanations ── visible payoff from existing data
8. D1 calibrated significance ── scores with a meaning
9. E2 cluster history → E5 watchlists ── operational product
10. E3 arch-neutral family signatures ── from clustering to detection
Why this order: - Moves 1–2 are forcing: cheap, low risk, and every later measurement gets better. Tuning anything else while name leakage and CRT noise are in the data would tune against artifacts. - Move 3 comes before C1 and C6, so the opt-axis and compiler work have a target to hit. - Moves 4–6 are development. Without them every new tier makes the batch run slower, so the position gets worse with each feature added. B1 is cheap and is a prerequisite for B2, E3 and E10. - Move 7 cashes in data that already exists: per-pair tier counts, every function match, and statement hash lists. Users see value while the deep work continues. - Moves 8–10 turn SimilarityDB from "a similarity database" into "a lineage and detection system". Signatures (E3) need the stable cores that cluster history (E2) provides.
Best evaluation after the line: the engine is online at 10⁹ scale; Windows lineage is clean; recall across compilers has been measured, and its weaknesses are known; similarity has a calibrated meaning; analysts get diffs, explanations, cluster timelines, alerts and architecture-neutral signatures.
Side line (run in parallel, research): C1 (-O0 normalization), then C3 / C4 (context and rarity tiers), then D4 (adversarial axis). Each is measured on the benchmark from move 3 before it is promoted to a default tier.
| line | why pruned |
|---|---|
| Learned embeddings (Gemini, Asm2Vec, jTrans) and an approximate-nearest-neighbour index | Illegal move: breaks the no-ML / explainability rule |
| LLM-written match explanations | Illegal: E7 gives deterministic explanations instead |
| More TLSH variants, or TLSH as a tier | Already refuted: no index (O(n²) at scale), and it carried 64% of inter-family weight. At most an experiment as a tie-breaker for large functions only |
| Deobfuscation passes (e.g. gooMBA) | Out of scope by design (removed in 5a08df9). D4 measures degradation instead |
| Dynamic analysis / sandbox traces | Binaries are deleted after analysis, and it isn't deterministic. A different product |
| A second lifter (e.g. Ghidra P-code) as a cross-check | Promising but expensive, and it can only run at ingestion time. Revisit after B5 shows how unstable IDA versions are |
| Network alignment (QBinDiff) instead of greedy matching + propagation | Unbounded cost per pair at scale. At most a per-pair refinement in the diff view (E1) |
| Per-family thresholds learned from labels | Drifts toward ML. D1's calibration gives one meaningful global threshold |
--df-unit
cluster. With B2 the engine must keep df per unit incrementally, or the defence lapses.These are less certain and depend on how moves 1–10 turn out: