CLASSIC watches what your VM program actually does, not what you call it. A recognized action fills a meter, and the meter drains again when the action stops. Those meters give the knight limited attack, survival, and collection power. The knight must still fight and collect gems to earn EXP.
Input: The VM actions in one tick.
Output: Meter values and limited power for the knight.
Question: How do chain checks and time change that power?
Attach CLASSIC to the model we already understand
We already have a pilot that selects actions, a VM that runs it, and a world that turns those actions into score. CLASSIC adds a side path. After each tick, it checks the VM's work, records named actions, and changes their meters.
Those meters can give the knight more power, but the knight's basic job stays the same. The pilot must still scan, move, and aim. The world must still defeat enemies and collect gems before EXP increases.
A chain is a named check of one or more VM behaviors. VECTOR, for example, checks a jump through an address loaded from memory. Each chain has one check and one meter. A family groups related chains. When the game sets a power limit, it counts both the active chains and their different families. So eight chains from one family give a lower limit than eight chains from four families.
What CLASSIC does each tick
A headerless upload selects CLASSIC. While the bot runs, the VM records its actions. When the VM stops for that tick, CLASSIC checks those records and changes the chain meters. The world then turns the meters into limited knight power before it handles attacks and gem pickups.
Keep the four jobs apart: the VM records, the host checks, the meter changes, and the world uses the result. One neat instruction trace does not instantly make the knight all-powerful. A successful tick gives only one meter point. The chain must keep passing for many ticks before its meter gives full power, and even then the world limits it.
Follow one VECTOR tick before learning all 41 chains
VECTOR is one CLASSIC chain: a jump whose address came from a memory load. A later section shows how to build that jump. For now, just follow one tick in the order the work happens. Along the way the bot also fills three proof cells. Think of them as a small challenge-and-answer game, not a secret way to edit EXP.
Before the bot runs, the host puts this tick's VECTOR challenge at 0xFE8C.
The bot reads the word, runs the answer formula, writes the result at 0xFE90, and leaves VECT at 0xFE94. A loaded-target jump makes the VM record computed_jump: “the bot used a computed jump.”
After the VM stops for this tick, CLASSIC checks the jump note, magic, and fresh answer together. All three pass → VECTOR's meter gains one. Any miss → it loses three, stopping at zero.
The meter remembers past ticks; the VM's jump note does not.CLASSIC calculates limited knight boosts from the meters. The world then moves enemies, resolves attacks, and handles gem pickups.
A boost can help a fight. Only a picked-up gem adds EXP.0 → 1 / 512Proof accepted. With only VECTOR active, the knight's attack fields still stay at their ordinary values.255 → 256 / 512VECTOR now counts toward how much power the game will allow; its own attack factor is 1.25. With no other active chain, this is the first tick its attack power can reach the knight.511 → 512 / 512VECTOR reaches its full 1.5 factor. Miss later ticks and the meter drains three at a time.Timing detail: the bot's movement request happened in step 02. A movement bonus first calculated in step 03 cannot change that earlier request; it can affect a later tick. Combat and gem pickup happen in step 04, after the new attack and collection values are calculated.
What those meter marks do in a real fight
The halfway mark is easy to miss. VECTOR's factor grows before its meter reaches 256, but the attack boost first reaches the knight at 256. The next figure shows one level-one knight facing one 14-HP spider at three points in that climb.
Assume that VECTOR is the only active chain, the spider is in range, and an attack is ready. The figure shows what one successful hit would do, not a complete match.
VECTOR's answer passed, but it does not count toward the attack limit yet. One ordinary hit leaves the spider at 2 HP; the knight still needs another hit.
VECTOR now counts. Its 1.25 attack factor produces 45 damage, which replaces the ordinary 12. One successful hit can defeat this spider. The full-circle attack is still locked.
VECTOR's factor reaches 1.5. One successful hit can defeat the spider, and the attack can cover a capped circle around the knight. Range, timing, and enemy position still matter.
Defeating the spider only drops the 3-EXP gem. The score goes up when the knight collects it. The sprites are from the game. Their positions in this diagram are example positions.
Why does the result change so suddenly at 256? The host adds a chain to K only when its meter is at least 256. With VECTOR at 1, K = 0, so the knight keeps its ordinary 12-damage attack.
At 256, K = 1 and VECTOR's attack factor is 1.25, so the damage becomes 180 × (1.25 − 1) = 45. At 512, the same calculation gives 180 × (1.5 − 1) = 90. This calculated damage replaces the ordinary 12 damage; it is not an extra 45 or 90 on top.
A full-circle attack needs an attack factor of at least 1.4. That is why the 256-point version still uses the ordinary aimed shape.
Families describe kinds of machine understanding
The CLASSIC table has 41 chains, and each chain carries two labels. Its family names the kind of VM action the chain checks: jumps, stack work, changed instructions, or bytes with two uses.
Its reward type, which the code calls an axis, names the knight power it can increase: attack, survival, or collection. The family affects the power limit, while the axis selects which power receives the chain's factor.
Take VECTOR. It checks a jump, so it belongs to the control-flow family, and its factor increases attack. PIVOT, by contrast, checks stack movement and increases survival. One label tells you what the bot did; the other tells you which knight power can change.
The map shows every family-and-axis pair that exists in this release. Open a box to see its chains and build steps.
Each row shows a type of VM behavior. Each column shows the knight power it can help. Open a box to read its chains.
THR · AttackShieldSRV · SurvivalMagnetHRV · CollectionCF · Jumps and returnsSword11 chainsVECTOR, CROWN + 9 more
STACK · Saved words and callsSMC · Written instruction bytesREINT · Code read as data or re-enteredShield1 chainJANUS
Magnet5 chainsPOLYGLOT, KALEIDOSCOPE + 3 more
These boxes come from the source table. An empty box has no chain. A chain must pass on repeated ticks to fill its meter.
Notice that all eleven control-flow chains increase attack, while stack chains spread across all three powers. The game assigns these axes, so you cannot move VECTOR to another axis by changing its proof cells. And even an accepted chain adds one point only to its own meter. It does not give full power on its first accepted tick.
Sustained participation, not a permanent toggle
Each chain has a meter that keeps its value across ticks. A passing tick adds one point, and a tick without that chain removes three. The meter always stays between 0 and 512, so finding a behavior once cannot give a permanent full-strength bonus.
The meter sets the chain's current factor, and the engine multiplies factors separately for attack, survival, and collection. An absent chain has a factor of 1, which changes nothing. That is why ordinary bots behave the same way while CLASSIC is watching.
The engine also keeps two counts. K counts chains with meters of at least 256, and Fam counts the different families among those chains. The limit uses Keff = min(K, 2 × Fam), so chains from one family cannot raise the limit as far as a varied set can.
For example, eight qualifying chains from one family give Keff = 2, while eight from four families can give Keff = 8. This example starts after the host has accepted the chains; it does not show how to activate them.
Work one meter through time
Once the host has decided which chains passed this tick, the following code updates every meter. Watch how a chain gains or loses points on each tick.
for i in 0..NCHAINS {
let delta = if armed[i] { 1 } else { -DECAY };
self.r[i] = (self.r[i] as i32 + delta).clamp(0, R_FULL as i32) as u16;
}DECAY is 3, and R_FULL is 512. Starting from zero, 300 accepted ticks put the meter at 300. Twenty absent ticks then remove 60 points and leave 240, which is below the 256-point count threshold. At 60 ticks per second, those twenty ticks take only one third of a simulated second.
One accepted CLASSIC contribution over time
Starts at zero; accepted ticks add 1, absent ticks subtract 3; the meter is clamped to 0–512. This models reward persistence after host validation.
A full meter takes 512 continuously accepted ticks to build from zero, about 8.53 seconds at 60 ticks per second. Going the other way, 85 absent ticks take a full meter down to 257, and the next one leaves 254, below the count threshold. So the bot must keep doing the action, even after the chain has reached full strength once.
As the meter falls, the chain factor shrinks gradually. The family count is different, because it uses a cutoff at 256. Crossing that line can lower the maximum knight power all at once.
Convert a meter into an axis factor
Why this formula? A chain that passes only on some ticks should give less power than one that passes on every tick.
Let r be the meter value and F the factor at a full meter. First calculate tau = r / 512, the fraction of a full meter, and then calculate 1 + (F - 1) * tau.
The code stores these values as Q16.16 fixed-point integers, a format in which 16 bits hold the fractional part:
#[inline]
pub(crate) fn tau_fx(r: u16) -> i32 {
((r as i64 * ONE as i64) / R_FULL as i64) as i32
}
#[inline]
pub(crate) fn f_fx(f_ceiling: i32, tau: i32) -> i32 {
ONE + fxmul(f_ceiling - ONE, tau)
}Meter r |
Participation | Current factor when F = 1.5 |
|---|---|---|
| 0 | 0 | 1.000 |
| 128 | 0.25 | 1.125 |
| 256 | 0.5 | 1.250 |
| 512 | 1 | 1.500 |
The engine multiplies this factor into the chain's assigned axis, but it counts family breadth only once the meter reaches 256. If other chains already allow a boost, this factor can therefore help even before its own meter reaches 256.
Work family breadth through the cap rule
For each chain at 256 or more, the code sets a family bit. It counts those bits to get Fam, then calculates Keff:
if self.r[i] >= R_HALF {
k += 1;
fam_mask |= 1 << CHAIN_FAMILY[i];
}
}
let fam = fam_mask.count_ones();
let keff = k.min(2 * fam) as i32;Eight counted chains in one family give K=8, Fam=1, and Keff=2. Spread the same eight across four families and Keff=8. Same chain count, very different room for power. That is why the CLASSIC apex's eight chains from four families matter.
One chain with a meter of at least 256
All eight in one family
K = 8 chains, Fam = 1 family
Keff = min(8, 2 × 1) = 2Lots of activity but little variety, so the effective count is only two.Two in each of four families
K = 8 chains, Fam = 4 families
Keff = min(8, 2 × 4) = 8Same number of chains. More variety raises the cap's room.This diagram starts after each chain has reached a meter value of at least 256. It shows the reward cap rule, not how to trigger those chains. A meter below 256 still gives some factor, but its chain adds no chip here.
Three axes with different jobs
Attack, which the code calls throughput, changes damage, attack area, hit passes, and attack timing. Survival changes armor, invulnerability time, and movement. Collection, called harvest in the code, changes gem pickup reach and pull speed. None of the three multiplies EXP directly.
Attack can defeat enemies faster and make more gems, but a dead knight collects none of them. Survival keeps the knight alive, yet it cannot make a missed attack hit. Collection helps only after gems appear. Power has to survive the whole trip from hit to pickup, since the score rises only when the knight collects a gem.
Hard caps are part of the game design
fold_levers combines the factors and applies maximum values. More active chains can raise those maximums, but the effects always stay limited. More chains also increase HEAT, so the knight faces more danger as its power grows.
A1 is this release's balance change. It multiplies CLASSIC's bonus limits by 13/20 but leaves the older VM behavior checks unchanged. CLASSIC still accepts the same chain actions; its maximum power simply sits below the later ABYSS steps.
The score does not necessarily fall by exactly 13/20. A smaller attack circle can miss enemies that used to be in range. Lower damage can require another attack, and a later level increase can delay a heal that saved the run. Each change then affects the ticks after it. That is why the release notes warn against guessing scores between measured points.
Why the factor product is not a score multiplier
Suppose two active attack chains give factors 1.2 and 1.5, for a product of 1.8. The game uses 1.8 to calculate limited combat values; it does not multiply final EXP by 1.8. Hits, deaths, and gem pickups still follow their own rules.
A spider with 14 HP needs two hits when each hit does 12 damage. It still needs two at 13 damage, and only at 14 does one hit suffice. Even then, EXP increases only after a gem pickup. Other thresholds jump in the same way. An attack can suddenly reach another enemy, armor can stop a hit, or one gem can cause a level increase.
So you cannot multiply an old score by the A1 limit to predict a new one. The new limits change where the knight moves and which enemies it hits. Those changes alter later healing and gem supply, so CLASSIC power and pilot movement must be studied together.
Follow one accepted chain into a picked-up gem
Here is a complete payout example. It starts after VECTOR has reached a full meter; a later section shows how to make it pass on each tick. All other chain meters are zero. The knight is level one, and an early 14-HP spider is in attack range. Follow the result from attack power all the way to EXP.
| Link | Calculation | What changes |
|---|---|---|
| VECTOR meter | r = 512, so tau = 512 / 512 = 1 |
Its full attack factor is 1.5 |
| Attack axis | R_thr = 1.5; the other attack-chain factors are 1 |
One chain has raised attack power, not EXP directly |
| Breadth limit | K = 1, Fam = 1, Keff = min(1, 2 × 1) = 1 |
The cap has only one chain's headroom |
| Attack circle | Raw radius is 60 + floor(150 × 0.5) = 135; A1 caps it at floor(125 × 13 / 20) = 81 |
The knight gets a local 81-pixel full circle, not a map-wide hit |
| Per-hit damage | floor(180 × 0.5) = 90, below the A1 damage cap of 227 |
The stored bonus_dmg becomes 90; CLASSIC uses it instead of normal 12 damage for this hit |
| Timing and cost | Cooldown shave is 15 ticks; extra passes remain zero; one active chain makes incoming HEAT about 1.12× |
The knight can attack sooner but also takes more danger |
The fold_levers code sets exactly these limited attack values. Notice the two separate checks. First R_thr must reach the level that enables the circle attack, and then Keff and A1 limit the circle radius.
let disc_cap = a1_cap((120 + 5 * keff).min(160));
let swings = (1 + keff / 2).min(6);
let armor_cap = a1_cap((8 + 5 * keff).min(48));
if k > 0 {
let committed = r_thr >= THR_DISC_UNLOCK_FX;
self.hero.disc_committed = committed;
self.hero.disc_radius = if committed {
(floor_scale(THR_DISC_COEF, r_thr - ONE) + 60).clamp(1, disc_cap)
} else {
0
};
self.hero.extra_swings = ((r_thr >> 16) - 1).max(0).min(swings - 1) as u32;
self.hero.swing_cd_shave = floor_scale(THR_CD_COEF, r_thr - ONE).min(a1_cap(28));
self.hero.bonus_dmg =
floor_scale(THR_DMG_COEF, r_thr - ONE).min(a1_cap(DMG_INJECT_CAP));If an attack reaches the spider, 90 damage defeats its 14 HP in one hit. To decide whether it reaches, CLASSIC compares the enemy's distance with the circle radius plus the enemy radius. It therefore cannot hit an enemy elsewhere on the map:
} else {
let query = radius + from_int(60);
let enemies = &self.enemies;
grid.for_each_near(px, py, query, |i| {
let e = enemies[i];
if !e.alive {
return;
}
let d = fx_dist(px, py, e.x, e.y);
if d <= radius + e.radius {
has_reach = true;
targets.push(i);
}
});The spider's death can create a gem worth 3 EXP. The knight still has to move close enough to collect it. Only then does total EXP rise by 3, which might cause a level change that heals the knight.
If the attack misses, the gem pool is full, or the knight never collects the gem, this example adds zero EXP. VECTOR improves a local fight; only a collected gem increases the score.
Why the instrumentation is separate
While the VM runs the bot, it records selected actions without changing any of the bot's calculations. CLASSIC reads those records only after the VM slice ends. This keeps the VM's instruction behavior separate from the game's reward checks.
For an ordinary compiled bot, CLASSIC's record system should therefore not change normal VM behavior. The repository tests this by comparing recorded runs of selected ordinary bots byte for byte. The claim applies to those tested bots.
The 41 chains, grouped by machine idea
The host checks each chain after the VM's work for a tick. Keep three things separate: what the VM noticed, whether the host accepted the chain, and the meter that remembers it across ticks. In this guide, an “exploit” means a recognized guest-VM action. It never writes directly to host memory or EXP.
Read the hand-off before the catalogue
After the VM runs, it hands CLASSIC a copy of its records. CLASSIC never reads the VM's private record buffers. A new or stopped VM reports false and zero for every record, so it cannot pass a chain by accident. Here is the hand-off:
pub fn pwn_latches(&self) -> PwnLatches {
let (aulos_word, aulos_high) = self.pack_sp_peaks();
PwnLatches {
computed_jump: self.computed_jump,
smc_exec: self.smc_exec,
smc_addr: self.smc_addr,
smc_orig: self.smc_orig,
overlap_fired: self.overlap_fired,
overlap_witness: self.overlap_witness,
sp_marshal: self.sp_marshal,
saw_raw_op: self.saw_raw_op,
exec_off_image: self.exec_off_image,
cfi_violation: self.cfi_violation,
ret_gadget_depth: self.ret_gadget_depth,
dispatch_max: self.dispatch_max,
jit_dispatch: self.jit_dispatch,In that copy, computed_jump: self.computed_jump carries the VM's yes-or-no jump record to CLASSIC. CLASSIC simply reads this value; it does not run the bot again to check the jump. The record type starts every field at false or zero:
See the default snapshot fields
#[derive(Clone, Copy, Default)]
pub struct PwnLatches {
pub computed_jump: bool,
pub smc_exec: bool,
pub smc_addr: u32,
pub smc_orig: u8,
pub overlap_fired: bool,
pub overlap_witness: u32,You have seen what a full meter can do. That raises the next question: how does a chain earn a single point? The 41 cards below give the checks for each chain, and you do not need to read them in order.
Pick one and ask three questions. What must the bot do? Which values must it write? Can it repeat both steps next tick? A VM record alone is only the first step.
The three locks on a CLASSIC chain
The host adds one chain point only when all three conditions are true in the same tick:
- Run the VM action named on the card.
- Put the chain's four-byte magic number at its specified address. Guest memory keeps this value across ticks, so an earlier write can still count.
- Read this tick's challenge from bot memory, calculate the answer, and write it to the specified address. Last tick's answer will usually fail.
The usual answer is econ_hash(challenge XOR ECHO), where each chain has a fixed ECHO value and the host changes the challenge every tick. A few chains put the answer somewhere else, and their cards explain where.
| VM behavior occurred? | Magic correct? | Fresh answer correct? | This chain's meter |
|---|---|---|---|
| Yes | Yes | Yes | Gains 1, up to 512 |
| Any other combination | — | — | Loses 3, down to 0 |
Try VECTOR as a concrete example. Before the bot runs, the host writes a new word at 0xFE8C. The bot must then jump to a loaded address, put VECT at 0xFE94, and put the new answer at 0xFE90. Change one option at a time below, and the three checks show exactly why the tick passes or fails.
Will VECTOR get credit this tick?
Choose what the program does and what it leaves in memory. The host checks both after the bot runs.
0xFE8C0xFE90Choose what the bot does, then let the host check this tick.
The sample challenges are invented. “This tick's answer” means the bot reads 0xFE8C, calculates econ_hash(challenge XOR ECHO), and stores it. The magic can stay in memory from an earlier tick. The VM forgets the jump when the next tick starts, and the answer must match the new challenge. Selecting a button does not itself perform a VM instruction.
This board starts VECTOR's meter at 4 so that you can see a three-point loss. In a new match the meter starts at zero, and a failed check there leaves it at zero.
The sample challenge words are invented, but the answer calculation and the three host checks follow the real code. The options show the cell values at check time; they do not simulate each memory write.
pub(crate) fn econ_hash(x: u32) -> u32 {
let mut h = x as i32;
h ^= h >> 16;
h = h.wrapping_mul(0x7FEB_352Du32 as i32);
h ^= h >> 15;
h = h.wrapping_mul(0x846C_A68Bu32 as i32);
h ^= h >> 16;
h as u32
}The bot does not need the secret KEY_SEED that makes the challenge. It can read the new challenge straight from memory. A fixed answer might pass one test tick and then fail on the next.
Put the proof in a loop alongside movement and aim. When its 10,000 instructions run out, the VM keeps its position and memory and continues on the next game tick. A HALT instead stops it permanently. CLASSIC clears the VM records every tick, so the bot must repeat both the action and the answer.
How to invent VECTOR from the code
During the CTF, the handout did not include the raw assembler or this source code. A player could still compare small compiled bots to learn their instruction bytes. Next, inspect the runner for VM instructions that the compiler never emits. Those findings were enough to build a raw .bin file by hand.
Now that the repository shows the implementation, we can check each step of that reasoning. We will build VECTOR in three passes. First make the VM record a jump, then satisfy the host proof, then repeat both without stopping the bot.
Start with the host's VECTOR check, which requires computed_jump, the VECTOR magic number, and this tick's answer. Then search the VM code for the line that sets computed_jump. Ask one small question: which jump makes that value true?
The answer lies in a taint flag that the VM keeps beside each stack value. A tainted value came from a memory load or a calculation, while a literal PUSH value is clean.
JMP_ABS and CALL_ABS check that flag when they take an address from the stack. The smallest VECTOR action is simple: store an ordinary label address in memory, load it back, and jump to it. The destination can be normal code, because VECTOR checks where the address came from, not where it leads.
Pass 1: make one computed jump
Before you read any bytes, try the two paths below. Both jump to the same address 18. Only the path through LOAD tags the address as computed, so only that path sets computed_jump. Neither path writes the host's magic or answer yet.
Same destination, different origin
Choose how the bot prepares address 18, then run one VM instruction at a time. Watch the tag beside the top stack value. This small test does not write VECTOR’s proof cells.
Now encode the loaded path as a complete 19-byte image. PUSH is byte 01 followed by a four-byte little-endian number, while STORE, LOAD, JMP_ABS, and HALT take one byte each. The target is offset 18, where HALT sits. Scratch address 0x0204 lies safely outside this image, and the longer loop later uses 0x0200 for its hash value.
| Image offset | Raw bytes | What the VM does |
|---|---|---|
0 |
01 12 00 00 00 |
Push destination 18. |
5 |
01 04 02 00 00 |
Push scratch address 0x0204. |
10 |
08 |
Store 18 at 0x0204. |
11 |
01 04 02 00 00 |
Push 0x0204 again. |
16 |
07 |
Load the saved 18; the VM tags this stack value. |
17 |
11 |
Jump to the tagged 18; computed_jump becomes true. |
18 |
FF |
Stop this one-shot test. |
Write these bytes to a raw .bin file. In a VM with record output, check that execution reaches offset 18 with computed_jump = true. If it does not, correct the bytes or the stack order before you add the answer calculation.
The released grader does not show this internal flag, so a player would compare program variants through the full host check instead. Note that this one-tick image does not earn VECTOR: it has no magic or answer, and HALT stops the bot.
pub const PUSH_ZERO: u8 = 0x03;
pub const POP: u8 = 0x04;
pub const LOAD_REL: u8 = 0x05;
pub const STORE_REL: u8 = 0x06;
pub const LOAD: u8 = 0x07;
pub const STORE: u8 = 0x08;
pub const JMP_REL: u8 = 0x10;
pub const JMP_ABS: u8 = 0x11;
pub const CALL_REL: u8 = 0x12;
pub const CALL_ABS: u8 = 0x13;
pub const SKIP_IF: u8 = 0x14;
pub const RET: u8 = 0x15;
pub const SYSCALL: u8 = 0x20;
pub const GATECALL: u8 = 0x21;
pub const ADD: u8 = 0x30;
pub const SUB: u8 = 0x31;
pub const MUL: u8 = 0x32;
pub const DIV_U: u8 = 0x33;
pub const REM_U: u8 = 0x34; fn push_imm(code: &mut Vec<u8>, v: u32) {
code.push(op::PUSH);
code.extend_from_slice(&v.to_le_bytes());
}| Step | Top-of-stack target | VM note | Why |
|---|---|---|---|
push resume |
Literal, clean | No VECTOR yet | The immediate comes straight from the image. |
push 0x0204; st |
Stored in scratch | No VECTOR yet | STORE pops address first, then the value. |
push 0x0204; ld |
Loaded, tainted | Still no VECTOR | LOAD marks its result as computed. |
jmpa |
Tainted target is popped | computed_jump = true |
The VM checks that popped slot before moving ip. |
0x0204LOAD from 0x0204target becomes taintedJMP_ABScomputed_jump = true0xFE8Cbot runs econ_hashanswer at 0xFE90magic at 0xFE94Both paths reach the host check in the same tick → VECTOR meter +1
op::LOAD => {
let a = self.pop();
self.note_arena_load(a);
self.note_read_own_code(a);
let v = self.mem.load_u32(a);
self.push_tagged(v, true);
}
op::STORE => {
let a = self.pop();
let v = self.pop();
self.note_arena_store(a);
self.note_code_store(a, v);
self.mem.store_u32(a, v); op::JMP_ABS => {
let t = self.pop();
let s = ((self.sp & ADDR_MASK) >> 2) as usize;
let tainted = self.taint[s >> 3] & (1u8 << (s & 7)) != 0;
if tainted {
self.computed_jump = true;
if self.is_interior_addr(t) {
self.note_hydra(ip_op, t);
}
}
self.note_indirect_target(ip_op, t, false);
self.clear_jit();
self.ip = t;
}The two lines to compare are push_tagged(v, true) in LOAD and if tainted { self.computed_jump = true; } in JMP_ABS. First LOAD marks the stack value, and later the jump reads that mark. Together, those two actions are the whole VECTOR VM record.
The repository also has a one-tick test of the complete check. It writes the magic, a calculated answer, and a HALT address in scratch memory. That test uses 0x0200 for scratch. Our longer loop needs 0x0200 for its hash value, so it keeps the jump address at 0x0204.
The choice of scratch address does not set the taint flag; the loaded jump does. The VM takes the tagged jump, the host accepts the test, and r[0] becomes 1.
Read this test as the smallest complete example, not a scoring bot. The test can call the host's private challenge function and then stop. A competitor bot cannot do either: it must read the live challenge and keep running.
fn build_vector_poc(tick: u32) -> Vec<u8> {
const SCRATCH: u32 = 0x0200;
const HALT_ADDR: u32 = 40;
let mut code = Vec::new();
push_imm(&mut code, VEC_MAGIC);
push_imm(&mut code, VEC_MAGIC_ADDR);
code.push(op::STORE);
push_imm(&mut code, vec_response(vec_challenge(tick)));
push_imm(&mut code, VEC_RESP_ADDR);
code.push(op::STORE);
push_imm(&mut code, HALT_ADDR);
push_imm(&mut code, SCRATCH);
code.push(op::STORE);
push_imm(&mut code, SCRATCH);
code.push(op::LOAD);
code.push(op::JMP_ABS);
assert_eq!(
code.len() as u32,
HALT_ADDR,
"HALT must land at the precomputed target"
);
code.push(op::HALT);
code
} #[test]
fn vector_full_poc_arms_the_meter_through_the_vm() {
let mut w = World::new(0x1234_5678, true);
w.begin_tick();
assert_eq!(
w.tick, 1,
"begin_tick sets tick to 1, matching the PoC's response"
);
let code = build_vector_poc(w.tick);
let mut vm = Vm::new(&code);
w.arm_vec_challenge(&mut vm.mem);
vm.run_tick(&mut w);
let l = vm.pwn_latches();
w.apply_exploit_staging(&mut vm.mem, l);
assert!(
vm.computed_jump,
"the LOAD-tainted JMP_ABS target trips the computed-jump detector"
);
assert_eq!(
w.r[0], 1,
"the raw-bytecode VECTOR PoC arms the VECTOR sustain meter +1 end-to-end"
);
assert_eq!(
w.econ_k, 0,
"a single armed tick is not yet a sustained chain"
);In a live run, the host writes VECTOR's new challenge at 0xFE8C. Your bot writes its answer at 0xFE90 and the VECT magic (0x56454354) at 0xFE94. The ECHO value (0x56656331) belongs in the calculation, not in a proof cell. If you swap ECHO and magic, the host rejects VECTOR even when the jump itself passes.
Pass 2: make the host accept the jump
Keep the jump through the loaded scratch value, and add the proof around it. Read the current word at 0xFE8C, XOR it with 0x56656331, and run econ_hash on the result. Then store that answer at 0xFE90 and store 0x56454354 at 0xFE94.
The host checks all three conditions after the VM slice ends. You can write the magic before or after the jump. All three conditions just have to be true when the host checks this tick.
| One-tick experiment | computed_jump |
Proof cells | VECTOR meter |
|---|---|---|---|
Direct PUSH target; JMP_ABS |
No | Correct | Falls by 3. |
| Scratch-loaded target, stale answer | Yes | Wrong | Falls by 3. |
| Scratch-loaded target, current answer and magic | Yes | Correct | Rises by 1. |
An indirect jump alone is therefore only a candidate. The host accepts VECTOR, and adds a meter point, only when the VM record and the memory proof agree.
pub const VEC_CHAL_ADDR: u32 = 0xFE8C;
pub const VEC_RESP_ADDR: u32 = 0xFE90;
pub const VEC_MAGIC_ADDR: u32 = 0xFE94;
pub const VEC_MAGIC: u32 = 0x5645_4354;
pub const VEC_ECHO: u32 = 0x5665_6331;
pub const VEC_DMG_CAP: i32 = 40;
pub const K_VECTOR: u64 = 0x5645_4354_0000_0005;Before the long assembly block, here is one worked answer for the invented challenge 0x12345678. The bot first calculates 0x12345678 XOR 0x56656331 = 0x44513549 and then runs the five hash steps below. The final word, 0x535FD5C0, goes into 0xFE90 for this tick only, because the host writes a new challenge next tick.
| Hash step | Value after the step |
|---|---|
| Start: challenge XOR ECHO | 0x44513549 |
| XOR with signed right-shift by 16 | 0x44517118 |
Multiply by 0x7FEB352D, keeping 32 bits |
0x97C2D938 |
| XOR with signed right-shift by 15 | 0x683DF6BD |
Multiply by 0x846CA68B, keeping 32 bits |
0x535F869F |
| XOR with signed right-shift by 16 | 0x535FD5C0 |
Signed right-shift keeps the sign bit as it moves bits right. Keeping 32 bits means that overflow wraps instead of making a larger number. Your bot must follow both rules exactly, or its answer will not match the host's.
Pass 3: turn the one-shot proof into a live loop
The assembly below repeats four tasks. It writes the magic, reads and answers the current challenge, then stores and reloads the loop address. Finally it jumps through that loaded address. Each repeated push 0x0200; ld pair just reads the current hash value back from scratch memory.
This is a VECTOR proof core, not a complete scoring bot. A full pilot must also scan, aim, and move. If your larger image uses 0x0200 for code, pick another safe scratch address.
loop:
push 0x56454354
push 0xFE94
st
push 0xFE8C
ld
push 0x56656331
xor
push 0x0200
st
push 0x0200
ld
push 0x0200
ld
push 16
shrs
xor
push 0x0200
st
push 0x0200
ld
push 0x7FEB352D
mul
push 0x0200
st
push 0x0200
ld
push 0x0200
ld
push 15
shrs
xor
push 0x0200
st
push 0x0200
ld
push 0x846CA68B
mul
push 0x0200
st
push 0x0200
ld
push 0x0200
ld
push 16
shrs
xor
push 0xFE90
st
push loop
push 0x0204
st
push 0x0204
ld
jmpaThe last ld is the key difference from a plain push loop; jmpa. Notice also that the live loop has no HALT. At the next tick, the VM resumes at its current instruction address, possibly halfway through the hash. For that reason, keep the proof cycle short enough to finish at least once in every 10,000-instruction slice.
Watch r[0] for several ticks. A steady +1 means the loop both triggers the behavior and writes the proof. A −3 tick means one part went missing. Once this works, you can add other chains to the same loop. Each one still needs its own VM behavior, magic value, and current challenge.
fn vector_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.computed_jump || mem.load_u32(VEC_MAGIC_ADDR) != VEC_MAGIC {
return false;
}
let r = mem.load_u32(VEC_RESP_ADDR);
r != 0 && r == vec_response(vec_challenge(self.tick))
}| Label | Meaning |
|---|---|
| CF / STACK / SMC / REINT | The machine-behavior family used for breadth |
| THR / SRV / HRV | The reward product receiving the interpolated factor |
| Full-meter factor | Ceiling before interpolation and knight-effect caps |
Use the same build method for the other 40
Each card gives the VM action and memory proof for one chain. To build one yourself, follow the same passes as VECTOR:
- Read the host check. Find every
&&condition in the embedded code. Note the magic and response cells, plus any address or count the check compares. - Find what sets each VM note. Search the VM code for the note's field name to see which instruction or event sets it. Typical causes are
LOAD, jumps,STORE, fetches, and stack moves. A value that merely looks right does not set the note. - Build a small one-shot image. Trigger only the required VM note first. If the check needs a count, use distinct addresses or destinations and confirm the exact threshold. Make sure the run reaches the host check.
- Write the current proof. Use this tick's challenge, answer, magic, and any witness cells. Most chains use the usual hash rule. A few put the answer in stack state or a saved instruction address.
- Repeat every tick. Add the behavior and proof to the live pilot loop, then test several ticks. One accepted tick adds only 1 to a meter that can reach 512.
The cards spell out the actions and numbers, so you do not have to invent them from the chain names. For a complex chain, build and test its smaller parts first. Make DISPATCH work before ROSETTA, for example, and PACKER before BASILISK. Each card also includes the exact host check, so you can compare your bot with the code.
Find a named chain · 41 entries
Select a chain to see its VM behavior and recorded fields. Its axis label shows which reward it affects.
Control flow
Stack
Self-modification
Reinterpretation
- JANUS SRV
- POLYGLOT HRV
- KALEIDOSCOPE HRV
- NARCISSUS HRV
- YGGDRASIL THR
- GLYPH HRV
- DOPPELROLL HRV
- SPHINX THR
If 41 chains feel like a lot, take a short tour of four first. VECTOR asks where a jump address came from, and NULLARBOR asks what unsigned arithmetic did at an edge case. PHOENIX asks whether changed code actually ran. JANUS asks whether a byte served first as data and later as an instruction.
All four use the same three-part proof; only the required VM behavior changes.
Control flow: where execution goes
For each jump, the VM can answer four separate questions, and different chains check different answers. Where did the target value come from, and which jump instruction used it? Did the target lie outside the uploaded program, and was it the start of an original instruction?
pub computed_jump: bool,
pub smc_exec: bool,
pub smc_addr: u32,
pub smc_orig: u8,
pub overlap_fired: bool,
pub overlap_witness: u32,
pub sp_marshal: u32,
pub saw_raw_op: bool,
pub exec_off_image: bool,
pub cfi_violation: bool,
pub ret_gadget_depth: u32,
pub dispatch_max: u32,
pub jit_dispatch: bool,
pub sp_lo: u32,
pub pivot_depth: u32,Three tiny jumps, three different VM notes
These are three separate one-tick test programs, not three steps to paste into one bot. Each answers a different question about a jump. VECTOR checks where the jump target came from. GADGET checks whether the target was an original instruction start. TRAMPOLINE checks whether the VM actually fetched an instruction beyond the uploaded image, and one jump can set more than one of these notes.
VECTOR probe: Save the ordinary continuation address 18 in scratch memory, LOAD it, and then use JMP_ABS. Address 18 starts a valid instruction in this image, so the destination itself is unremarkable. What matters is that the VM marks the loaded value as computed and therefore sets computed_jump.
GADGET probe: The repository's VM test starts with PUSH 0x0000FF00, whose five bytes are 01 00 FF 00 00: opcode 01 at offset 0, then four value bytes. The original decoder therefore sees one instruction starting at 0. The byte FF at offset 2 is just part of its value.
The test then runs PUSH 2; JMP_ABS, and the VM reads that FF at offset 2 as HALT. Offset 2 was not an original instruction start, so the branch-target check sets cfi_violation, even though offset 2 is still inside the uploaded image:
let mut a = Asm::new();
a.push(0x0000_FF00);
a.push(2);
a.op(op::JMP_ABS);
let code = a.finish();
let mut vm = Vm::new(&code);
vm.run_tick(&mut NopHost); fn note_indirect_target(&mut self, site: u32, t: u32, is_call: bool) {
if t >= self.code_end || !self.is_instr_start(t) {
self.cfi_violation = true;
}The direct PUSH 2 target does not count as computed for VECTOR. Because it also stays inside the image, it does not trigger TRAMPOLINE either. The interior fetch can, however, set byte-overlap notes, so “GADGET happened” does not mean that only GADGET happened.
TRAMPOLINE probe: Another VM test writes FF to guest address 0xD000, pushes 0xD000, and jumps there. The uploaded image ends well before that address. Because the jump target is outside the image, the GADGET boundary check also records cfi_violation. Then, when the VM fetches the FF at 0xD000, it sets exec_off_image for TRAMPOLINE. Writing the byte without fetching it would not set that note:
let mut a = Asm::new();
a.push(0xFF);
a.push(0xD000);
a.op(op::STORE);
a.push(0xD000);
a.op(op::JMP_ABS);
let code = a.finish();
let mut vm = Vm::new(&code);
vm.run_tick(&mut NopHost); if (a as u32) >= self.code_end {
self.exec_off_image = true;
}These tests end with HALT and never write a chain's magic value or current answer. They show how VM notes arise, not how to score over hundreds of ticks. To earn meter marks, add the chain's proof cells, then repeat the behavior in a live loop.
DISPATCH asks a fourth question: how many distinct destinations did one jump instruction reach in the same tick? Its card below builds eight stubs that all return to one shared jump instruction.
VECTOR#00 · CF · THR · full-meter 1.5
What it checks. A computed jump gets its destination from a value that the bot calculated or loaded while it ran. The VM records that origin. The destination can still be inside the uploaded image.
| VM note | What it means |
|---|---|
computed_jump | The branch destination came from a computed value, not only a compiler-emitted literal. |
Build and activate it
- Store an ordinary in-image continuation address in scratch memory, then
LOADit and feed that loaded value toJMP_ABS. The load marks the target stack slot as computed; a directPUSH label; JMP_ABSleaves it clean. - Repeat that indirect jump in the live pilot loop. The full annotated example immediately above this catalogue supplies the exact hash and memory writes.
Proof for this tick
0xFE8C0x56454354 → 0xFE940xFE900x56656331Read this chain’s exact host check
fn vector_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.computed_jump || mem.load_u32(VEC_MAGIC_ADDR) != VEC_MAGIC {
return false;
}
let r = mem.load_u32(VEC_RESP_ADDR);
r != 0 && r == vec_response(vec_challenge(self.tick))
}CROWN#04 · CF · THR · full-meter 2.5
What it checks. CROWN is a combo challenge. In one tick, the VM must show four kinds of unusual behavior. One kind gives it two notes, so the host checks five notes in all, plus CROWN's own fresh answer. You do not have to pass the four smaller chains first.
Save a safe code address, LOAD it, then use JMP_ABS.
computed_jumpPUSH valueAfter PUSH consumes four value bytes, jump into one and fetch it as an opcode.
overlap_firedAlter a byte inside the uploaded image, then actually fetch that changed byte.
smc_execRun an opcode the compiler never emits. Begin a SYSCALL with its frame at 0xD000–0xFFFF.
saw_raw_opsp_marshalRead [0xFEC4] → XOR 0x43726F31 → econ_hash → write [0xFEC8]. Put CRWN at [0xFECC].
After the VM stops: all five notes + CROWN's magic + its nonzero fresh answer pass → r[4] rises by 1. Miss one → it falls by 3, stopping at zero.
| VM note | What it means |
|---|---|
computed_jump | The branch destination came from a computed value, not only a compiler-emitted literal. |
overlap_fired | Execution decoded a byte inside a region previously consumed as an immediate. |
saw_raw_op | Execution used a raw opcode path distinct from the compiler’s usual output. |
smc_exec | A changed byte in the original image was later fetched as an instruction. |
sp_marshal | The stack address at which a host-call frame was marshalled. |
Two useful details: one byte can set both overlap_fired and smc_exec. That happens when the VM first consumes it as PUSH data and the bot then changes it and fetches it as an opcode. The VM still records five separate facts.
Because its notes reset next tick, all the required behaviors must occur together in one tick. Other chain meters still need their own proof cells, even when they reuse these actions.
Build and activate it
- Make the four behaviors work separately first. Reuse VECTOR’s loaded-target jump, fetch a byte from a
PUSHvalue, and fetch a changed image byte. Then run a raw-only opcode such asDIV_U, and start a call with its stack frame in the upper arena. Check each VM note before you combine them. - Put all four behaviors in one VM loop and complete them within one 10,000-instruction tick slice. Do not
HALT, because the next tick needs the same evidence again. One fetch can count as both overlap and changed-code execution, but a write without a fetch counts as neither. - Add CROWN's proof: read this tick’s word at
0xFEC4, calculateecon_hash(challenge XOR 0x43726F31), and write the nonzero result at0xFEC8. Write0x4352574E(CRWN) at0xFECC, and recalculate the answer after each new challenge. - Test all five notes with the proof present, then remove one behavior at a time; CROWN must stop rising. The source test checks each note this way. Only CROWN’s meter is guaranteed to rise. VECTOR, JANUS, PHOENIX, and PIVOT need their own checks and proof cells.
Proof for this tick
0xFEC40x4352574E → 0xFECC0xFEC80x43726F31The ECHO goes into the calculation, not another cell.
Read this chain’s exact host check
fn crown_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.smc_exec
&& l.overlap_fired
&& l.computed_jump
&& l.sp_marshal >= SP_PIVOT_ARENA_LO
&& l.sp_marshal < VM_MEM_SIZE as u32
&& l.saw_raw_op)
|| mem.load_u32(CAP_MAGIC_ADDR) != CAP_MAGIC
{
return false;
}
let r = mem.load_u32(CAP_RESP_ADDR);
r != 0 && r == cap_response(cap_challenge(self.tick))
}Read the one-note-at-a-time test
fn crown_negatives_leave_the_meter_idle() {
let droppers: [(&str, fn(&mut PwnLatches)); 5] = [
("smc_exec", |l| l.smc_exec = false),
("overlap_fired", |l| l.overlap_fired = false),
("computed_jump", |l| l.computed_jump = false),
("sp_marshal", |l| l.sp_marshal = SP_PIVOT_ARENA_LO - 1),
("saw_raw_op", |l| l.saw_raw_op = false),
];
for (name, drop) in droppers {
let (mut w, mut mem) = armed();
mem.store_u32(CAP_MAGIC_ADDR, CAP_MAGIC);
mem.store_u32(CAP_RESP_ADDR, cap_response(cap_challenge(w.tick)));
let mut l = crown_latches();
drop(&mut l);
w.apply_exploit_staging(&mut mem, l);
assert_eq!(w.r[4], 0, "CROWN missing {name} ⇒ never armed");
}TRAMPOLINE#05 · CF · THR · full-meter 1.5
What it checks. The uploaded image fills only part of guest memory. TRAMPOLINE requires the VM to fetch an instruction beyond that image. This still runs inside the guest VM; it is not the host computer executing arbitrary native code.
| VM note | What it means |
|---|---|
exec_off_image | Instruction fetch crossed outside the originally loaded image extent. |
Build and activate it
- Put executable VM opcode bytes into guest memory beyond the uploaded image end, such as the free arena near
0xD000.STOREwrites four bytes at a time, little-endian. - Branch there and actually fetch at least one opcode. Merely writing that memory or pointing at it without fetching cannot set
exec_off_image. Return or loop rather than ending onHALTif you need a meter.
Proof for this tick
0xFED00x54524D50 → 0xFED80xFED40x54726D31Read this chain’s exact host check
fn trampoline_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.exec_off_image || mem.load_u32(TRAMP_MAGIC_ADDR) != TRAMP_MAGIC {
return false;
}
let r = mem.load_u32(TRAMP_RESP_ADDR);
r != 0 && r == tramp_response(tramp_challenge(self.tick))
}GADGET#06 · CF · THR · full-meter 1.5
What it checks. GADGET requires a branch to an address that was not an instruction start in the original image. From that new starting point, the VM decodes the bytes differently. It watches the mismatch between the jump target and the original boundary map, and the jump stays inside the guest VM. It is not evidence that the bot controls the host instruction pointer or native host memory.
| VM note | What it means |
|---|---|
cfi_violation | A control transfer landed away from an original decoded instruction start. |
Build and activate it
- Make an original
PUSHinstruction whose immediate bytes contain legal small opcodes; for example a byte0x03isPUSH_ZERO. Normal linear decoding treats those bytes as data, not instruction starts. - Branch with
JMP_ABSorCALL_ABSto an interior byte. The branch-target checker compares it with the original instruction-start bitmap and raisescfi_violation. An off-image destination also passes this checker, but an interior landing demonstrates the intended mismatch.
Proof for this tick
0xFEDC0x47444754 → 0xFEE40xFEE00x47646731Read this chain’s exact host check
fn gadget_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.cfi_violation || mem.load_u32(GDGT_MAGIC_ADDR) != GDGT_MAGIC {
return false;
}
let r = mem.load_u32(GDGT_RESP_ADDR);
r != 0 && r == gdgt_response(gdgt_challenge(self.tick))
}DISPATCH#07 · CF · THR · full-meter 1.7
What it checks. Picture one jump instruction acting like a switchboard: on different visits, it sends execution to different places. DISPATCH counts the distinct destinations reached from that same site during a tick. Jumps from different instruction addresses do not combine.
| VM note | What it means |
|---|---|
dispatch_max | Maximum distinct targets seen from one dispatch site in the tick. |
Build and activate it
- Build eight safe destination stubs, each returning to one shared dispatcher. Keep the same byte address for the dispatcher’s
JMP_ABSinstruction. - On visits to that one site, load or calculate a different target and jump. The per-site ledger deduplicates targets, so eight visits to one target count as one; eight different targets make
dispatch_max ≥ 8. The ledger resets each tick.
Proof for this tick
0xFEE80x44535054 → 0xFEF00xFEEC0x44737031Read this chain’s exact host check
fn dispatch_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.dispatch_max < 8 || mem.load_u32(DISP_MAGIC_ADDR) != DISP_MAGIC {
return false;
}
let r = mem.load_u32(DISP_RESP_ADDR);
r != 0 && r == disp_response(disp_challenge(self.tick))
}RAMPART#08 · CF · THR · full-meter 2.0
What it checks. RAMPART follows a sequence of saved return addresses outside the original image. The VM must return through distinct off-image targets in one tick.
| VM note | What it means |
|---|---|
ret_gadget_depth | Depth of a return-routed chain through off-image destinations. |
Build and activate it
- Write eight
RETopcodes at distinct addresses outside the uploaded image. Arrange stack return words so oneRETlands on the next off-imageRET, then finally back on a safe continuation. - Execute that route. The VM counts distinct off-image return targets, not the number of
RETopcodes or normal in-image function returns. The required depth is eight in one tick.
Proof for this tick
0xFEF40x524D5054 → 0xFEFC0xFEF80x526D7031Read this chain’s exact host check
fn rampart_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.ret_gadget_depth < 8 || mem.load_u32(RMPT_MAGIC_ADDR) != RMPT_MAGIC {
return false;
}
let r = mem.load_u32(RMPT_RESP_ADDR);
r != 0 && r == rmpt_response(rmpt_challenge(self.tick))
}GANTRY#09 · CF · THR · full-meter 1.8
What it checks. GANTRY requires generated guest code and repeated calls from one instruction address. That call site must reach different off-image destinations in one tick.
| VM note | What it means |
|---|---|
jit_dispatch | One call site dispatched into multiple generated guest-code destinations. |
Build and activate it
- Write four off-image callable blocks that end in
RET. Make one loop visit the sameCALL_ABSinstruction four times. - Give that same call site four different off-image target addresses. The VM counts only off-image
CALL_ABStargets. It needs four distinct targets from one site in one tick. Calls from separate sites do not combine.
Proof for this tick
0xFF000x474E5452 → 0xFF080xFF040x476E7431Read this chain’s exact host check
fn gantry_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.jit_dispatch || mem.load_u32(GNTR_MAGIC_ADDR) != GNTR_MAGIC {
return false;
}
let r = mem.load_u32(GNTR_RESP_ADDR);
r != 0 && r == gntr_response(gntr_challenge(self.tick))
}WYRM#32 · CF · THR · full-meter 3.0
What it checks. WYRM combines control-flow, stack, call-frame, and byte-decoding observations, and the host checks them together in one tick. Passing one of those checks alone is not WYRM. Its attack meter has a larger factor, but it still fades and the final bonus still has a cap.
| VM note | What it means |
|---|---|
dispatch_max | Maximum distinct targets seen from one dispatch site in the tick. |
marshal_alias | A marshalled call frame intersected another reserved memory role. |
overlap_density | Distinct interior bytes decoded as opcodes, with repeated visits deduplicated. |
pivot_depth | High-water displacement of the stack pointer, derived from its peak. |
raw_argc_wide | The raw argument count exceeded the normal width before masking. |
ret_gadget_depth | Depth of a return-routed chain through off-image destinations. |
How the fields fit: The host combines the jump and return notes. For one part of the check, either marshal_alias or raw_argc_wide can supply the needed observation. WYRM has its own meter; the smaller related chains keep theirs.
Build and activate it
- Combine eight distinct off-image return targets from RAMPART with twelve targets from one DISPATCH site. Fetch sixteen distinct interior bytes as opcodes. Complete all three parts in one tick.
- Add a LOCKSMITH marshal alias or a MIRAGE raw argument count. Move the stack high-water into
[0xD000, 0x10000). All five parts must appear in one tick, because events from separate ticks do not combine. - Test each part separately before you combine them. The dispatch ledger holds sixteen destinations, so twelve fits. Return to the pilot to repeat WYRM.
Proof for this tick
0xFD000x5759524D → 0xFD080xFD040x57797231Read this chain’s exact host check
fn wyrm_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
let sp_hi_in_arena = l.pivot_depth >= (SP_PIVOT_ARENA_LO - SP_INIT)
&& l.pivot_depth < (VM_MEM_SIZE as u32 - SP_INIT);
if !(l.ret_gadget_depth >= WYRM_RET_MIN
&& l.dispatch_max >= WYRM_DISPATCH_MIN
&& l.overlap_density >= WYRM_OVERLAP_MIN
&& (l.marshal_alias || l.raw_argc_wide)
&& sp_hi_in_arena)
|| mem.load_u32(WYRM_MAGIC_ADDR) != WYRM_MAGIC
{
return false;
}
let r = mem.load_u32(WYRM_RESP_ADDR);
r != 0 && r == wyrm_response(wyrm_challenge(self.tick))
}ROSETTA#34 · CF · THR · full-meter 2.5
What it checks. ROSETTA requires a computed branch and many destinations from the same site. Stored command bytes alone do not prove that the VM used them for control flow.
| VM note | What it means |
|---|---|
computed_jump | The branch destination came from a computed value, not only a compiler-emitted literal. |
dispatch_max | Maximum distinct targets seen from one dispatch site in the tick. |
How the fields relate: The computed-branch note records the origin of a target. The dispatch ledger counts targets from one site. It does not count every branch in the bot. ROSETTA joins that breadth with computed origin and its own response evidence. Neither observation alone establishes the interpreter-like history or awards another chain's meter.
Build and activate it
- Extend DISPATCH to sixteen destinations from one site in one tick. This is the ledger’s limit. Use
LOADor arithmetic to compute the target beforeJMP_ABSorCALL_ABS. - Check both
dispatch_max ≥ 16andcomputed_jump. The current checker does not ask for a separate “heavy compute” signal despite the decrypt-dispatch theme.
Proof for this tick
0xFD180x524F5345 → 0xFD200xFD1C0x526F7331Read this chain’s exact host check
fn rosetta_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.dispatch_max >= ROSETTA_DISPATCH_MIN && l.computed_jump)
|| mem.load_u32(ROSETTA_MAGIC_ADDR) != ROSETTA_MAGIC
{
return false;
}
let r = mem.load_u32(ROSETTA_RESP_ADDR);
r != 0 && r == rosetta_response(rosetta_challenge(self.tick))
}METRONOME#37 · CF · THR · full-meter 1.6
What it checks. METRONOME checks where the VM saves its next instruction address when a tick budget ends. The VM must also execute beyond the uploaded image.
| VM note | What it means |
|---|---|
exec_off_image | Instruction fetch crossed outside the originally loaded image extent. |
resume_ip | The saved outer-VM instruction pointer used when a budget slice resumes. |
Gate and carrier: Fetching outside the original image establishes the execution context. The saved resume_ip identifies the continuation point at the budget boundary and carries the chain-specific response. It is not a second independent chain contribution.
Build and activate it
- Read the current challenge and compute
park = 0xD000 + (econ_hash(challenge XOR METRO_ECHO) & 0x1FFF). Make the VM execute off-image and consume its entire 10,000-instruction tick slice. - End the tick budget with
resume_ipat that exact park address. An off-image self-loop can show one budget cut. To keep the meter, read each new challenge and select a new park address. The saved address is the answer; there is no response cell.
Proof for this tick
0xFD380x4D455452 → 0xFD3C0x4D657431No response cell: the saved off-image IP carries the challenge-derived park address.
Read this chain’s exact host check
fn metronome_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.exec_off_image || mem.load_u32(METRO_MAGIC_ADDR) != METRO_MAGIC {
return false;
}
l.resume_ip != 0 && l.resume_ip == metro_park(metro_challenge(self.tick))
}HYDRA#40 · CF · THR · full-meter variable; ≤ 2.4
What it checks. HYDRA counts distinct interior-byte destinations reached from one call site. A larger count raises this chain’s reward limit, up to the chain’s cap.
| VM note | What it means |
|---|---|
hydra_phase | Number of distinct interior re-entries reached from one tainted site. |
Build and activate it
- Execute
PUSH 0x15151515, marking its four immediate bytes as interior; each byte is aRETopcode if entered as code. Preserve a loop with one sharedCALL_ABSsite. - Load or calculate two different interior addresses of that
PUSHand call them from the same site. The tainted targets makecomputed_jump; each gadget returns, allowing the site to run again. HYDRA requires at least two distinct interior re-entries and scales further up to five.
Proof for this tick
0xFD580x48594452 → 0xFD600xFD5C0x48796431Read this chain’s exact host check
fn hydra_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.hydra_phase < HYDRA_PHASE_MIN || mem.load_u32(HYDRA_MAGIC_ADDR) != HYDRA_MAGIC {
return false;
}
let r = mem.load_u32(HYDRA_RESP_ADDR);
r != 0 && r == hydra_response(hydra_challenge(self.tick))
}Stack: which storage role a word occupies
The VM uses one flat memory array for operands, call frames, host-call arguments, and code. Its stack notes record where a word sat and how it was used, not merely that a number was large.
pub arena_frames: u32,
pub frames_verified: u32,
pub marshal_alias: bool,
pub raw_argc_wide: bool,
pub syscall_reentrant: bool,
pub divzero_u: bool,
pub shift_annihilate_u: bool,
pub signedness_split: bool,
pub split_count: u32,
pub wrap_carry: bool,
pub code_patched: bool,Worked machine trace: one array, several roles
Think about a function call that stores a local value and makes a host request. Within one 64 KiB array, arithmetic values, a return frame, and host-call arguments each take a different role.
| Moment | Observed state | Why it is recorded |
|---|---|---|
| Call begins | Frame stored at a stack address | Location and depth can be tracked (arena_frames, pivot_depth) |
| Value later loaded | A matching stored frame is used | frames_verified distinguishes a store/load history from a call count |
| Host request marshalled | Arguments occupy a memory range | sp_marshal records its location; marshal_alias records overlap with another role |
| Following service reads arguments | Earlier return state might still occupy a consumed slot | syscall_reentrant concerns lifetime, not stack depth |
A deep stack, several related call frames, and host-call arguments overlapping other data are three different behaviors. The CLASSIC chain named ABYSS is about the stack, and it is separate from the ABYSS engine.
PIVOT#03 · STACK · SRV · full-meter 1.5
What it checks. A host call reads its arguments from the bot's stack. Move the stack pointer, and the host reads those arguments from a different part of guest memory. PIVOT checks the stack address used for a host-call frame. It requires that frame in the upper guest-memory arena together with a raw-only opcode, and the addresses stay inside the VM.
| VM note | What it means |
|---|---|
saw_raw_op | Execution used a raw opcode path distinct from the compiler’s usual output. |
sp_marshal | The stack address at which a host-call frame was marshalled. |
Gate and location: The raw-operation observation establishes the relevant low-level execution context. sp_marshal records where the host-call frame was actually read. PIVOT's host decision relates that address to the alternate arena; it is not simply a check that the stack pointer moved.
Build and activate it
- Move the operand stack into the arena. From
sp=0x8000, 5,120 net word pushes reach0xD000. The VM has no ordinary “set SP” instruction. A unit test setsvm.spdirectly only to test the detector. - Execute a
SYSCALLwhile its pre-pop marshal base is at least0xD000and below0x10000. Also execute a raw-only opcode, for exampleDIV_U. Parked stack state carries across ticks, but proof latches reset.
Proof for this tick
0xFEB80x50495654 → 0xFEC00xFEBC0x50697631Read this chain’s exact host check
fn pivot_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.sp_marshal >= SP_PIVOT_ARENA_LO && l.sp_marshal < VM_MEM_SIZE as u32)
|| !l.saw_raw_op
|| mem.load_u32(PIV_MAGIC_ADDR) != PIV_MAGIC
{
return false;
}
let r = mem.load_u32(PIV_RESP_ADDR);
r != 0 && r == piv_response(piv_challenge(self.tick))
}ABYSS#10 · STACK · SRV · full-meter 1.6
What it checks. This card shares a name with the ABYSS engine, but it belongs to CLASSIC. It checks whether the outer VM's stack pointer moves below its starting address, which means the program popped more words than it pushed. The VM's memory bounds still apply.
| VM note | What it means |
|---|---|
sp_lo | Lowest stack-pointer position reached relative to its initial position. |
Build and activate it
- Use surplus
POPinstructions from the initialsp=0x8000. One pop takes the pointer below its starting value; the VM records a negative relative low-water mark. - This card is a CLASSIC stack chain named ABYSS, not the separate ABYSS engine. Plan where later pushes go, because underflow can wrap stack addresses and overwrite guest memory.
Proof for this tick
0xFF0C0x41425953 → 0xFF140xFF100x41627931Read this chain’s exact host check
fn abyss_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if (l.sp_lo as i32) >= 0 || mem.load_u32(ABYS_MAGIC_ADDR) != ABYS_MAGIC {
return false;
}
let r = mem.load_u32(ABYS_RESP_ADDR);
r != 0 && r == abys_response(abys_challenge(self.tick))
}PLUMB#11 · STACK · HRV · full-meter variable; ≤ 2.0
What it checks. PLUMB checks how far the stack pointer rises above its starting address. That depth affects the collection reward limit for this chain.
| VM note | What it means |
|---|---|
pivot_depth | High-water displacement of the stack pointer, derived from its peak. |
Build and activate it
- Increase the stack high-water at least
0x1000bytes above its0x8000starting point: 1,024 net four-byte pushes reach0x9000. - The host checks
pivot_depth = sp_hi − 0x8000, not a special opcode. A unit test directly setsvm.spto model this state; a submitted program has to get there through stack operations.
Proof for this tick
0xFF180x504C4D42 → 0xFF200xFF1C0x506C6D31Read this chain’s exact host check
fn plumb_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.pivot_depth < PLUMB_DEPTH_MIN || mem.load_u32(PLMB_MAGIC_ADDR) != PLMB_MAGIC {
return false;
}
let r = mem.load_u32(PLMB_RESP_ADDR);
r != 0 && r == plmb_response(plmb_challenge(self.tick))
}LOCKSMITH#12 · STACK · SRV · full-meter 1.7
What it checks. LOCKSMITH checks whether a host-call frame overlaps a reserved guest-memory role. The same guest bytes can then serve as call arguments and another kind of data.
| VM note | What it means |
|---|---|
marshal_alias | A marshalled call frame intersected another reserved memory role. |
Build and activate it
- Make the call frame overlap another guest-memory role. One unit test puts the call base at
0xFF00. Its argument-count slot at0xFEFCthen enters the reserved[0xFE00,0xFF00)page. - The recovered apex takes the code-region route. It pads its uploaded image to
code_end=0xD010and uses 5,120PUSH_ZEROinstructions to parkspat0xD000. A call from there reads its first word at0xCFFC, which the VM now counts as part of the image. - Execute a real
SYSCALL. After limiting the argument count, the VM checksbase−4·kfork=1..argc+2. An address in either reserved region setsmarshal_alias. Repeat the call each tick because the note resets. - Read the fresh challenge and answer it. Padding and a call alone do not pass the host's magic-and-response check. See the apex memory close-up for the exact address comparison.
Proof for this tick
0xFF240x4C4F434B → 0xFF2C0xFF280x4C6F6B31Read this chain’s exact host check
fn locksmith_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.marshal_alias || mem.load_u32(LOCK_MAGIC_ADDR) != LOCK_MAGIC {
return false;
}
let r = mem.load_u32(LOCK_RESP_ADDR);
r != 0 && r == lock_response(lock_challenge(self.tick))
}MIRAGE#13 · STACK · THR · full-meter 1.35
What it checks. MIRAGE checks the raw argument count of a host call before the VM limits that count. Raw VM code can request more arguments than the compiler normally emits.
| VM note | What it means |
|---|---|
raw_argc_wide | The raw argument count exceeded the normal width before masking. |
Build and activate it
- Put a raw argument count greater than eight on top of a syscall frame, for example
0x7FFFFFF8. Put a service number below it. ExecuteSYSCALL. - The detector reads the raw count before the VM applies
(raw_argc & 0xF).min(8). That example masks to eight, so prepare enough frame words or choose a wide value whose low nibble is convenient. The latch depends on the original count, not how many args the host finally receives.
Proof for this tick
0xFF300x4D495247 → 0xFF380xFF340x4D697231Read this chain’s exact host check
fn mirage_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.raw_argc_wide || mem.load_u32(MIRG_MAGIC_ADDR) != MIRG_MAGIC {
return false;
}
let r = mem.load_u32(MIRG_RESP_ADDR);
r != 0 && r == mirg_response(mirg_challenge(self.tick))
}OSSUARY#14 · STACK · THR · full-meter 1.55
What it checks. OSSUARY counts call frames used in the upper guest-memory arena. Bytes that only resemble a frame do not count; the VM must execute real calls.
| VM note | What it means |
|---|---|
arena_frames | Calls whose frames were placed in the alternate arena region. |
Build and activate it
- Move the operand stack into
[0xD000,0x10000). Run fourCALL_ABSinstructions while it stays there. Each short called block can return to the loop. - The VM increments
arena_frameswhen each call pushes its return address. Four calls are enough; four ordinary calls near0x8000are not.
Proof for this tick
0xFF3C0x4F535355 → 0xFF440xFF400x4F737331Read this chain’s exact host check
fn ossuary_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.arena_frames < ARENA_FRAMES_MIN || mem.load_u32(OSSU_MAGIC_ADDR) != OSSU_MAGIC {
return false;
}
let r = mem.load_u32(OSSU_RESP_ADDR);
r != 0 && r == ossu_response(ossu_challenge(self.tick))
}SEXTANT#15 · STACK · HRV · full-meter 1.75
What it checks. SEXTANT requires stores and later loads from the same distinct arena slots. The VM records both operations and counts the slots they share.
| VM note | What it means |
|---|---|
frames_verified | Frames observed on both the store and later load sides of the frame ledger. |
Build and activate it
- For eight distinct arena stride slots,
STOREa word and laterLOADfrom an address in that same slot. A simple set uses0xD000 + i·0x100fori=0..7. - The VM keeps separate stored and loaded bitmaps and counts their intersection. Eight stores alone, or eight loads from different slots, do not pass.
Proof for this tick
0xFF480x53455854 → 0xFF500xFF4C0x53657831Read this chain’s exact host check
fn sextant_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.frames_verified < FRAMES_VERIFIED_MIN || mem.load_u32(SEXT_MAGIC_ADDR) != SEXT_MAGIC {
return false;
}
let r = mem.load_u32(SEXT_RESP_ADDR);
r != 0 && r == sext_response(sext_challenge(self.tick))
}CATENARY#16 · STACK · THR · full-meter 1.8
What it checks. CATENARY requires one host call’s live return value to occupy part of a later host-call frame. Two unrelated calls do not satisfy that order.
| VM note | What it means |
|---|---|
syscall_reentrant | A live earlier host-call return was still present in a consumed argument slot. |
Build and activate it
- Execute a syscall and leave its returned word on the operand stack. Do not pop it or push over that slot, because either action clears its live-return mark.
- Construct the next syscall frame so the previous return occupies an argument or number slot that the second marshal reads. The detector checks live slots in that read range (
k≥2), not merely two syscalls in a row.
Proof for this tick
0xFF540x4341544E → 0xFF5C0xFF580x43617431Read this chain’s exact host check
fn catenary_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.syscall_reentrant || mem.load_u32(CATN_MAGIC_ADDR) != CATN_MAGIC {
return false;
}
let r = mem.load_u32(CATN_RESP_ADDR);
r != 0 && r == catn_response(catn_challenge(self.tick))
}NULLARBOR#17 · STACK · SRV · full-meter 1.6
What it checks. What should unsigned division by zero do inside this VM? The VM has a defined answer, so it does not crash, and NULLARBOR records that the operation occurred. Its STACK label is just a grouping for the economy. The division itself grants nothing; any bonus comes later from the chain's meter.
| VM note | What it means |
|---|---|
divzero_u | An unsigned divide or remainder encountered a zero divisor. |
Build and activate it
- Push a dividend and then zero. Execute raw
DIV_UorREM_U. The VM returns a defined result and recordsdivzero_u. - A signed divide or a normal nonzero divisor does not set this latch. Clean up the result before returning to the pilot.
Proof for this tick
0xFF600x4E554C4C → 0xFF680xFF640x4E756C31Read this chain’s exact host check
fn nullarbor_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.divzero_u || mem.load_u32(NULL_MAGIC_ADDR) != NULL_MAGIC {
return false;
}
let r = mem.load_u32(NULL_RESP_ADDR);
r != 0 && r == null_response(null_challenge(self.tick))
}Walk the eight bytes
Here is the smallest one-tick test. The first five bytes are PUSH 0xFFFFFFFF in little-endian order. The next three bytes push zero, divide, and stop.
01 FF FF FF FFPUSH 0xFFFFFFFF03PUSH_ZERO33DIV_UFFHALT| After instruction | Stack, bottom → top | VM note |
|---|---|---|
PUSH 0xFFFFFFFF | [0xFFFFFFFF] | None yet |
PUSH_ZERO | [0xFFFFFFFF, 0] | None yet |
DIV_U | [0] | divzero_u = true |
The division removes the top word first, so zero is the divisor. The VM returns zero instead of stopping with an error. More importantly for this chain, it records that unsigned division by zero happened.
0x30..=0x45 => {
let b = self.pop();
let a = self.pop();
self.note_alu_faults(opcode, a, b);
self.push_tagged(alu_bin(opcode, a, b), true);
}The code above pops b before a. The next two pieces show the two separate effects: a note for the host, and the zero result on the stack.
op::DIV_U | op::REM_U => {
if b == 0 {
self.divzero_u = true;
}
} op::DIV_U => {
if b == 0 {
0
} else {
a / b
}
}divzero_u = trueThe eight-byte test reaches this point.[0xFF68] = 0x4E554C4CWrite the chain's magic word.[0xFF64] = econ_hash([0xFF60] XOR 0x4E756C31)Read this tick's challenge, then write its answer.What the eight bytes prove: only step 1. The small image has no magic or answer, and HALT ends the bot. To earn NULLARBOR, put this arithmetic in the live pilot loop. Answer the new challenge each tick, and keep moving and collecting gems.
The host requires all three checks together, so one successful tick adds just one of the 512 meter points. DIV_U also sets the VM's general raw-op note, but NULLARBOR's host check asks for divzero_u, not that extra note.
TITHONUS#18 · STACK · SRV · full-meter 1.6
What it checks. A shift count can ask to move bits farther than a 32-bit word is wide. TITHONUS checks an unsigned right shift with a count of at least 32, for which the VM returns zero and records the oversized count. Your browser's number rules or your laptop's CPU may behave differently, so follow the VM's own shift rules here.
| VM note | What it means |
|---|---|
shift_annihilate_u | An unsigned right shift used a count outside the word width. |
Build and activate it
- Push an unsigned value and a shift amount at least 32, such as 40, then execute raw
SHR_U. The VM recordsshift_annihilate_uwhen the shift amount is out of the normal 0–31 range. - A signed shift or a shift of 31 is a different instruction history, even if the final number looks similar.
Proof for this tick
0xFF6C0x54495448 → 0xFF740xFF700x54697431Read this chain’s exact host check
fn tithonus_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.shift_annihilate_u || mem.load_u32(TITH_MAGIC_ADDR) != TITH_MAGIC {
return false;
}
let r = mem.load_u32(TITH_RESP_ADDR);
r != 0 && r == tith_response(tith_challenge(self.tick))
}AMBERHORN#19 · STACK · THR · full-meter 2.0
What it checks. AMBERHORN checks a difference between signed and unsigned readings of the same 32-bit values. A high-bit operand can also set the alternate accepted note.
| VM note | What it means |
|---|---|
signedness_split | Signed and unsigned comparisons of the same words disagreed. |
wrap_carry | A high-bit word appeared on the unsigned comparison path. |
How the fields fit: Either a signed-versus-unsigned disagreement or a high-bit wrap can supply this VM observation. The host still checks this chain's own response before its meter rises.
Build and activate it
- Run raw
CMP_LT_UorCMP_LE_Uwith operands on opposite sides of the sign bit. For example, use0x80000000and1. Signed and unsigned comparisons then disagree. - This sets
signedness_split; a high-bit operand on the same unsigned-compare path can also setwrap_carry. The host accepts either latch for AMBERHORN.
Proof for this tick
0xFF780x414D4252 → 0xFF800xFF7C0x416D6231Read this chain’s exact host check
fn amberhorn_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.signedness_split || l.wrap_carry) || mem.load_u32(AMBR_MAGIC_ADDR) != AMBR_MAGIC {
return false;
}
let r = mem.load_u32(AMBR_RESP_ADDR);
r != 0 && r == ambr_response(ambr_challenge(self.tick))
}STYX#33 · STACK · SRV · full-meter 2.5
What it checks. STYX combines a low stack position, a high stack position, arena call frames, and matched store/load slots. All required observations must occur in one tick.
| VM note | What it means |
|---|---|
arena_frames | Calls whose frames were placed in the alternate arena region. |
frames_verified | Frames observed on both the store and later load sides of the frame ledger. |
pivot_depth | High-water displacement of the stack pointer, derived from its peak. |
sp_lo | Lowest stack-pointer position reached relative to its initial position. |
How the fields relate: STYX requires low and high stack positions with a large span. It also requires arena call frames and matching stores and loads. A final stack pointer alone cannot establish that sequence. STYX has its own response check and meter; the individual observations do not automatically confirm ABYSS, OSSUARY, SEXTANT, or other stack chains.
Build and activate it
- Move
spbelow0x8000, then reach at least0xD000. The low-to-high span must be at least0x6000. A tick can start with the stack already high. Its currentspinitializes that tick’s high-water mark. - In the arena, run four
CALL_ABSframes. UseSTOREandLOADon eight matching stride slots, as OSSUARY and SEXTANT do. The low position, frames, and matched slots must occur in this tick. Earlier VM notes do not persist.
Proof for this tick
0xFD0C0x53545958 → 0xFD140xFD100x53747931Read this chain’s exact host check
fn styx_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
let sp_hi = l.pivot_depth as i64 + SP_INIT as i64;
let sp_lo_abs = SP_INIT as i64 + (l.sp_lo as i32) as i64;
let span = sp_hi - sp_lo_abs;
if !((l.sp_lo as i32) < 0
&& l.pivot_depth >= (SP_PIVOT_ARENA_LO - SP_INIT)
&& span >= STYX_SPAN_MIN
&& l.arena_frames >= ARENA_FRAMES_MIN
&& l.frames_verified >= FRAMES_VERIFIED_MIN)
|| mem.load_u32(STYX_MAGIC_ADDR) != STYX_MAGIC
{
return false;
}
let r = mem.load_u32(STYX_RESP_ADDR);
r != 0 && r == styx_response(styx_challenge(self.tick))
}JORMUNGAND#35 · STACK · THR · full-meter 1.9
What it checks. JORMUNGAND needs four arithmetic notes and the raw-op note in one tick. They cover unsigned zero division, an oversized unsigned shift, and signed-versus-unsigned comparison behavior.
| VM note | What it means |
|---|---|
divzero_u | An unsigned divide or remainder encountered a zero divisor. |
saw_raw_op | Execution used a raw opcode path distinct from the compiler’s usual output. |
shift_annihilate_u | An unsigned right shift used a count outside the word width. |
signedness_split | Signed and unsigned comparisons of the same words disagreed. |
wrap_carry | A high-bit word appeared on the unsigned comparison path. |
How the fields fit: The host checks all five VM notes and JORMUNGAND's own response. They describe different calculations. Seeing them does not automatically fill the separate meters for the smaller related chains.
Build and activate it
- Run
DIV_UorREM_Uwith divisor zero. RunSHR_Uwith a shift of at least 32. Run an unsigned comparison that differs from the signed result. Give it a high-bit operand to setwrap_carry. - All four arithmetic notes plus
saw_raw_opare required in one tick. The unsigned compare can establish bothsignedness_splitandwrap_carry; a wrapping multiply by itself cannot establish the latter.
Proof for this tick
0xFD240x4A4F524D → 0xFD2C0xFD280x4A6F7231Read this chain’s exact host check
fn jormungand_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.divzero_u
&& l.shift_annihilate_u
&& l.signedness_split
&& l.wrap_carry
&& l.saw_raw_op)
|| mem.load_u32(JORM_MAGIC_ADDR) != JORM_MAGIC
{
return false;
}
let r = mem.load_u32(JORM_RESP_ADDR);
r != 0 && r == jorm_response(jorm_challenge(self.tick))
}AULOS#36 · STACK · THR · full-meter 1.5
What it checks. AULOS records eight recent local stack peaks. The bot must shape those peaks to encode a response word from the current challenge.
| VM note | What it means |
|---|---|
aulos_high | How high the stack pointer reached. |
aulos_word | Several stack-high observations packed into one word. |
Gate and carrier: aulos_high says the stack reached a qualifying high-water state. aulos_word packs the stack's path and the chain's response. These fields describe one check over time, not two rewards.
Build and activate it
- Read the challenge and compute
econ_hash(challenge XOR AULOS_ECHO). Park the stack on a 64-byte-aligned arena base such as0xD000, then make eight push/pop excursions. - For each response nibble from least to most significant, choose an excursion height whose peak satisfies
(peak >> 2) & 0xF = nibble. Eight most-recent local peaks pack intoaulos_word; all must be at least0xD000. Arrange other stack-using work before those eight peaks so it does not overwrite the pattern. There is no response cell.
Proof for this tick
0xFD300x41554C53 → 0xFD340x41756C31No response cell: the eight stack peaks carry the ECHO-hash response.
Read this chain’s exact host check
fn aulos_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.aulos_high || mem.load_u32(AULOS_MAGIC_ADDR) != AULOS_MAGIC {
return false;
}
l.aulos_word == aulos_response(aulos_challenge(self.tick))
}HOURGLASS#38 · STACK · SRV · full-meter 1.6
What it checks. HOURGLASS checks whether off-image execution continues across consecutive tick-budget boundaries. The VM must save increasing off-image instruction addresses.
| VM note | What it means |
|---|---|
sled_straddle | A recorded execution span crossed a world-tick boundary. |
Build and activate it
- Enter a long off-image straight-line instruction sled and exhaust the 10,000-instruction slice without halting. At each boundary the VM saves its next IP.
- Make two consecutive increases in that saved off-image IP, which takes three budget cut points to establish. The unit test uses a
POPsequence. Each opcode advances one byte. A loop to one fixed address does not setsled_straddle. This is a short-lived primitive unless the program also solves the finite-address and re-entry problem.
Proof for this tick
0xFD400x484F5552 → 0xFD480xFD440x486F7531Read this chain’s exact host check
fn hourglass_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.sled_straddle || mem.load_u32(HOUR_MAGIC_ADDR) != HOUR_MAGIC {
return false;
}
let r = mem.load_u32(HOUR_RESP_ADDR);
r != 0 && r == hour_response(hour_challenge(self.tick))
}Self-modification: what bytes are actually fetched
Changing a code byte, fetching that changed byte, and counting how many changed addresses were fetched are three different events.
pub packer_count: u32,
pub metamorphic: bool,
pub metamorphic_addr: u32,
pub straight_line_jit: bool,
pub read_own_code: bool,
pub read_own_code_cov: u32,
pub overlap_density: u32,Worked machine trace: writing is not executing
Start with a code byte X in the uploaded image. During one tick, the bot writes a different value Y there. Keep these three events apart:
| Stage | Observation | What it cannot establish alone |
|---|---|---|
| Store | Original-image code byte differs (code_patched) |
That changed byte ever ran |
| Fetch | The VM later decodes that address (smc_exec) |
How many distinct changed addresses ran |
| Count | A deduplicated ledger grows (packer_count) |
Which route reached the altered code |
LOOM, PHOENIX, PACKER, and OUROBOROS each check a different part of this process. The host still checks each chain's proof before it adds a meter mark.
The VM's tested PACKER idea changes eight bytes. This teaching probe extends it to sixteen bytes for BASILISK, using the same operations. The VM counts distinct changed addresses that it later fetches. The probe runs once and does not score.
Image addresses 50–65 contain sixteen FF bytes. FF means HALT, but execution will not reach them until after the stores.
FF FF FF FF · FF FF FF FF · FF FF FF FF · FF FF FF FFFour word stores write 0x50505050 at addresses 50, 54, 58, and 62. Each word supplies four one-byte NOT instructions.
50 50 50 50 · 50 50 50 50 · 50 50 50 50 · 50 50 50 50Jump to 50. The VM fetches all sixteen changed NOTs, then the untouched FF at 66 stops the probe. The count rises once per changed address.
0–43Four repeats of PUSH 0x50505050, PUSH destination, STORE; destinations are 50, 54, 58, 62. Each repeat is eleven bytes.
44–49PUSH 50; JMP_ABS. This takes six bytes and lands on the first changed byte.
50–66Sixteen original FF bytes followed by one final FF. Only the first sixteen are rewritten.
The repository's VM test proves the eight-byte version (two stores, block starting at 28). This sixteen-byte version uses the same fetch-and-count rule; it is not a saved reference bot, and HALT ends it after one tick. To earn meter points, also write BASILISK's fresh answer and magic, then run the behavior inside a pilot that continues on later ticks.
The repository's eight-byte VM test is the source for this construction. It uses two stores and starts its sealed strip at address 28. Our sixteen-byte version adds two more stores, which moves the strip to address 50. Here is the test code:
const BASE: u32 = 28;
let mut code = Vec::new();
for w in 0..2u32 {
pimm(&mut code, 0x5050_5050);
pimm(&mut code, BASE + w * 4);
code.push(op::STORE);
}
pimm(&mut code, BASE);
code.push(op::JMP_ABS);
assert_eq!(code.len() as u32, BASE, "setup ends exactly at BASE");
for _ in 0..8 {
code.push(op::HALT);
}
code.push(op::HALT);PHOENIX#02 · SMC · THR · full-meter 1.5
What it checks. PHOENIX requires a change to an original-image byte and a later fetch of that changed byte. A data write without an instruction fetch does not count.
| VM note | What it means |
|---|---|
smc_addr | Address of the changed original-image byte used as a witness. |
smc_exec | A changed byte in the original image was later fetched as an instruction. |
smc_orig | The original byte at that witnessed location, before the change. |
Event and witness: smc_exec records that altered original-image code was fetched. smc_addr and smc_orig retain the location and prior byte for the host's witness check. A write alone belongs to the earlier stage of the history; this chain concerns the later execution.
Build and activate it
- Put an original
0xFF(HALT) byte in the image as a sealed rune. At run time, replace it withPUSH_ZEROor another safe opcode. Branch or continue to fetch that changed byte. - Write the rune address to
SMC_RUNE_ADDR. The host checks that the VM’s last recorded changed fetch has original byte0xFFand the same address. Another changed-code fetch after the rune can replacesmc_addrand spoil this check.
Proof for this tick
0xFEA80x50484E58 → 0xFEB00xFEAC0x52684E31Also write the sealed rune address to 0xFEB4.
Read this chain’s exact host check
fn phoenix_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.smc_exec
|| l.smc_orig != SMC_SEAL_BYTE
|| l.smc_addr != (mem.load_u32(SMC_RUNE_ADDR) & ADDR_MASK)
|| mem.load_u32(SMC_MAGIC_ADDR) != SMC_MAGIC
{
return false;
}
let r = mem.load_u32(SMC_RESP_ADDR);
r != 0 && r == smc_response(smc_challenge(self.tick))
}PACKER#20 · SMC · THR · full-meter 1.5
What it checks. PACKER counts distinct changed original-image addresses that the VM later fetches as instructions. Writing the bytes alone does not count.
| VM note | What it means |
|---|---|
packer_count | Number of distinct changed code addresses later fetched in the tick. |
Build and activate it
- Prepare at least eight original image bytes for changes. The VM test starts with eight
0xFFbytes. Two word stores replace them with eight one-byte0x50opcodes. The test then jumps into the changed block. - Fetch all eight changed addresses in one tick.
packer_countcounts distinct in-image addresses whose fetched bytes differ from the original image. Repeated visits to one address count once.
Proof for this tick
0xFF840x5041434B → 0xFF8C0xFF880x50636B31Read this chain’s exact host check
fn packer_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.packer_count < PACKER_COUNT_MIN || mem.load_u32(PACK_MAGIC_ADDR) != PACK_MAGIC {
return false;
}
let r = mem.load_u32(PACK_RESP_ADDR);
r != 0 && r == pack_response(pack_challenge(self.tick))
}LOOM#21 · SMC · THR · full-meter 1.5
What it checks. LOOM checks a write that changes an original-image code byte. It does not require the VM to fetch the changed byte afterward.
| VM note | What it means |
|---|---|
code_patched | A guest store changed a code-region byte from its original image value. |
Build and activate it
- Use
STOREto change at least one byte in the original image region. Select an unused image area to keep the bot running. - The VM sets
code_patchedat write time. No jump to the patched bytes is needed for LOOM; that extra fetch belongs to PHOENIX or PACKER.
Proof for this tick
0xFF900x4C4F4F4D → 0xFF980xFF940x4C6F6D31Read this chain’s exact host check
fn loom_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.code_patched || mem.load_u32(LOOM_MAGIC_ADDR) != LOOM_MAGIC {
return false;
}
let r = mem.load_u32(LOOM_RESP_ADDR);
r != 0 && r == loom_response(loom_challenge(self.tick))
}BASILISK#22 · SMC · THR · full-meter variable; ≤ 1.6
What it checks. BASILISK requires many distinct changed image bytes to run as instructions. The number of changed addresses sets its reward limit.
| VM note | What it means |
|---|---|
packer_count | Number of distinct changed code addresses later fetched in the tick. |
Build and activate it
- First test the sixteen-byte example above. Four word stores change sixteen original-image bytes. A jump fetches all sixteen changed opcodes in one tick. The unchanged final
HALTstops that test. For a live bot, return to the pilot instead. - Check
packer_count = 16. Sixteen trips through one changed address count only once. The host's BASILISK test needs at least sixteen different changed addresses plus its own magic and fresh response. - Repeat the work and proof in later ticks to fill the BASILISK meter. At count 16, its full-meter attack factor is about 1.075. Count 64 gives about 1.3, and count 128 gives 1.6. Other game caps still apply, and these values are not EXP multipliers.
Proof for this tick
0xFF9C0x4241534C → 0xFFA40xFFA00x42736C31Read this chain’s exact host check
fn basilisk_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.packer_count < BASILISK_COUNT_MIN || mem.load_u32(BASL_MAGIC_ADDR) != BASL_MAGIC {
return false;
}
let r = mem.load_u32(BASL_RESP_ADDR);
r != 0 && r == basl_response(basl_challenge(self.tick))
}OUROBOROS#23 · SMC · THR · full-meter 1.5
What it checks. OUROBOROS requires the VM to write a code byte and then reach that byte without a branch. The fetch must occur inside the armed window.
| VM note | What it means |
|---|---|
straight_line_jit | Changed code was fetched along a straight-line execution path. |
Build and activate it
- Store a changed opcode into the original image a few bytes ahead of the current IP. The VM test writes byte 13 while the following instruction starts at 11.
- Fall straight through and fetch the rewritten byte inside the armed window. Any branch between the store and fetch clears that special arm: you may still get
smc_exec, but notstraight_line_jit.
Proof for this tick
0xFFA80x4F55524F → 0xFFB00xFFAC0x4F757231Read this chain’s exact host check
fn ouroboros_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.straight_line_jit || mem.load_u32(OURO_MAGIC_ADDR) != OURO_MAGIC {
return false;
}
let r = mem.load_u32(OURO_RESP_ADDR);
r != 0 && r == ouro_response(ouro_challenge(self.tick))
}PROTEUS#24 · SMC · THR · full-meter 1.6
What it checks. PROTEUS requires one address to have two roles. The VM first consumes it as instruction data, then changes it and fetches it as an opcode.
| VM note | What it means |
|---|---|
metamorphic | An executed changed opcode also has a second role as an immediate byte. |
Build and activate it
- Execute a
PUSHto mark its four immediate bytes as interior. Overwrite one of those exact bytes with a different opcode and branch into that byte. - At the fetch, the VM records that the same address changed and was previously used as data. These conditions set
metamorphic. A code change at one address and an overlap at another do not count.
Proof for this tick
0xFFB40x50524F54 → 0xFFBC0xFFB80x50727431Read this chain’s exact host check
fn proteus_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.metamorphic || mem.load_u32(PROT_MAGIC_ADDR) != PROT_MAGIC {
return false;
}
let r = mem.load_u32(PROT_RESP_ADDR);
r != 0 && r == prot_response(prot_challenge(self.tick))
}SCRIVENER#39 · SMC · THR · full-meter 1.6
What it checks. SCRIVENER requires a host service to change guest code bytes, and the VM must then fetch changed in-image code during the same tick. It follows the change of role from service output to instruction; this is still guest memory, not native host-code execution.
| VM note | What it means |
|---|---|
host_code_patched | A host write changed a code-region byte from the original image. |
smc_exec | A changed byte in the original image was later fetched as an instruction. |
Origin and consumption: host_code_patched distinguishes a host-produced code-region change from a guest store. smc_exec records subsequent fetch of changed code. SCRIVENER joins that origin to later execution; seeing smc_exec does not automatically establish PHOENIX's separate witness check.
Build and activate it
- Point a scan service’s
result_addrinto an unused original-image region. Make the scan return a record that changes at least one image byte. The VM compares code before and after the host call. - Fetch a changed in-image byte during the same tick to set
smc_exec. The host checks two separate notes. The fetched byte need not be the byte the host wrote. A separate safe patch and fetch can supply this note.
Proof for this tick
0xFD4C0x53435256 → 0xFD540xFD500x53637231Read this chain’s exact host check
fn scrivener_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.host_code_patched && l.smc_exec) || mem.load_u32(SCRIV_MAGIC_ADDR) != SCRIV_MAGIC {
return false;
}
let r = mem.load_u32(SCRIV_RESP_ADDR);
r != 0 && r == scriv_response(scriv_challenge(self.tick))
}Reinterpretation: when one representation has several roles
A byte can first be instruction data and later become an instruction. A bot can also read its own code as data. The VM records the context that makes each second use meaningful.
pub g_word: u32,
pub resume_ip: u32,
pub sled_straddle: bool,
pub aulos_word: u32,
pub aulos_high: bool,
pub host_code_patched: bool,
pub hydra_phase: u32,
}
impl Vm {Worked machine trace: bytes do not have one permanent role
An opcode can use the following bytes as an immediate value. A jump can then make the VM start decoding at one of those bytes. The byte does not move; only the starting point changes. Reading code as data is a separate event again.
| Question | Observation | Example chain |
|---|---|---|
| Was an immediate byte later read as an opcode? | overlap_fired and witness |
JANUS |
| How many distinct interior bytes gained that role? | overlap_density |
POLYGLOT / KALEIDOSCOPE |
| Did the program inspect its code image as data? | read_own_code and coverage |
YGGDRASIL / NARCISSUS |
| Did several host output writes overlap? | writer_collision |
DOPPELROLL |
This family is about relationships among interpretations, so a single straight disassembly or one read cannot establish every named behavior.
JANUS#01 · REINT · SRV · full-meter 1.5
What it checks. An instruction holds an opcode and sometimes extra input bytes. Start decoding at a different position and a byte that was input can become an opcode. JANUS requires the VM to fetch as an opcode a byte it previously consumed as instruction data. The host then checks the address of that interior fetch.
| VM note | What it means |
|---|---|
overlap_fired | Execution decoded a byte inside a region previously consumed as an immediate. |
overlap_witness | The VM retains which overlapping byte supplied that observation. |
Event and witness: overlap_fired records the reinterpretation event. overlap_witness retains the particular interior byte used in the host's check. The witness anchors a claim about the event; it does not add a second independently metered chain.
Build and activate it
- Execute
PUSHto mark its four input bytes as data. Jump into one interior byte and fetch it as an opcode. Put safe opcodes in the input so execution can return. - The VM records the most recent interior fetch address as
overlap_witness. Store that exact nonzero address atOVL_WITNESS_ADDRbefore host checking. If you visit another interior byte later, update the witness cell or the host rejects JANUS.
Proof for this tick
0xFE980x4A414E55 → 0xFEA00xFE9C0x4F766C31Also write the latest overlap address to 0xFEA4.
Read this chain’s exact host check
fn janus_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !l.overlap_fired
|| l.overlap_witness == 0
|| mem.load_u32(OVL_WITNESS_ADDR) != l.overlap_witness
|| mem.load_u32(OVL_MAGIC_ADDR) != OVL_MAGIC
{
return false;
}
let r = mem.load_u32(OVL_RESP_ADDR);
r != 0 && r == ovl_response(ovl_challenge(self.tick))
}POLYGLOT#25 · REINT · HRV · full-meter 1.5
What it checks. The bytes stay the same, but execution that begins at different offsets decodes different instructions. POLYGLOT counts the distinct interior bytes fetched as opcodes. No code rewrite is needed for one program to have several readings.
| VM note | What it means |
|---|---|
overlap_density | Distinct interior bytes decoded as opcodes, with repeated visits deduplicated. |
Build and activate it
- First run the three aligned
PUSHinstructions in the byte walk below. Each marks four following addresses as data: twelve candidate addresses in these three regions. - Jump to
0x01. The three disguisedPOPs and one disguisedJMP_ABSat each landing take you to0x06, then0x0B. POLYGLOT's six-address requirement is crossed partway through the second landing. - Write this tick’s POLYGLOT magic and answer before the byte walk. Then return to the pilot instead of stopping at the example
HALT. Repeat the walk on later ticks to keep the meter.
Proof for this tick
0xFFC00x504F4C59 → 0xFFC80xFFC40x506C7931Read this chain’s exact host check
fn polyglot_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.overlap_density < OVERLAP_DENSITY_MIN || mem.load_u32(POLY_MAGIC_ADDR) != POLY_MAGIC {
return false;
}
let r = mem.load_u32(POLY_RESP_ADDR);
r != 0 && r == poly_response(poly_challenge(self.tick))
}One byte walk that reaches both overlap thresholds
This example is the missing bridge between “jump into a PUSH” and “count twelve different bytes.” It is a one-tick teaching program. It reaches the VM counter, but it writes neither chain's magic value nor answer, so it does not score.
0x00 PUSH0104020403040411one four-byte number; mark addresses 01–040x05 PUSH0604070408040911mark addresses 06–090x0A PUSH0B040C040D040E11mark addresses 0B–0E0x013 × POP; jump0x063 × POP; jump0x0B3 × POP; jump0x30HALTAll addresses and byte values in the diagram are hexadecimal. The small labels are addresses, and the large values are the bytes stored there. Byte 04 means POP, and 11 means JMP_ABS.
On the first pass, these are only part of a PUSH value, but the jump to 0x01 makes the VM read them as instructions. The bytes stay exactly the same; only the point where decoding begins moves.
Build the 49-byte route:
- Put
01 04 04 04 11at0x00,0x05, and0x0A. This makes three alignedPUSHinstructions. Each value hides threePOPinstructions and oneJMP_ABS. - Put
04 04 04at0x0F. ThesePOPinstructions remove the three values left by the alignedPUSHinstructions. Build the jump stack from bottom to top:0x30, 0, 0, 0, 0x0B, 0, 0, 0, 0x06, 0, 0, 0, 0x01. UsePUSHfor addresses andPUSH_ZEROfor each zero. - Put
JMP_ABSat0x2FandHALTat0x30. The first jump uses0x01. Each hidden group removes three zeros and jumps to the next group. The last group jumps toHALT.
| Image offset | Exact bytes | Job on the first pass |
|---|---|---|
00–04, 05–09, 0A–0E |
01 04 04 04 11 at each start |
Mark three four-byte value regions. |
0F–11 |
04 04 04 |
Clear those three pushed numbers. |
12–19 |
01 30 00 00 00 03 03 03 |
Place final address 0x30, then three zeros. |
1A–21 |
01 0B 00 00 00 03 03 03 |
Place third landing 0x0B, then three zeros. |
22–29 |
01 06 00 00 00 03 03 03 |
Place second landing 0x06, then three zeros. |
2A–30 |
01 01 00 00 00 11 FF |
Place first landing 0x01, jump, then keep a stop at 0x30. |
The later PUSH instructions also mark their own value bytes as interior, but nothing jumps into those, so they add zero to the count. The VM remembers fetched addresses, not repeat visits, so twelve jumps to 0x01 still count as one address. The VM does the bookkeeping here:
if is_interior {
self.overlap_fired = true;
self.overlap_witness = ipa;
if self.fetched_interior[a >> 3] & (1u8 << (a & 7)) == 0 {
self.fetched_interior[a >> 3] |= 1u8 << (a & 7);
self.overlap_density = self.overlap_density.saturating_add(1);
}
}One boundary is worth keeping straight. The twelve-address walk proves the VM behavior, but it does not by itself arm POLYGLOT or KALEIDOSCOPE. The host also checks each chain's magic value and current answer. For JANUS, the final interior address fetched here is 0x0E. Write 0x0E to its witness cell if you also want JANUS in that tick.
KALEIDOSCOPE#26 · REINT · HRV · full-meter 1.75
What it checks. KALEIDOSCOPE requires more distinct interior opcode fetches than POLYGLOT. It pays into collection and uses its own proof cells.
| VM note | What it means |
|---|---|
overlap_density | Distinct interior bytes decoded as opcodes, with repeated visits deduplicated. |
Build and activate it
- Use the exact three-landing walk above. Its twelve unique interior fetch addresses reach KALEIDOSCOPE's threshold in one tick; the first six also satisfy POLYGLOT's VM-side threshold.
- Write KALEIDOSCOPE’s magic and current answer before the byte walk. POLYGLOT uses different proof cells. Its answer cannot substitute for KALEIDOSCOPE’s answer.
- Replace the example
HALTwith a safe return to the pilot. Then repeat the byte walk on later ticks.
Proof for this tick
0xFFCC0x4B414C45 → 0xFFD40xFFD00x4B616C31Read this chain’s exact host check
fn kaleidoscope_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.overlap_density < KALEIDOSCOPE_DENSITY_MIN
|| mem.load_u32(KALE_MAGIC_ADDR) != KALE_MAGIC
{
return false;
}
let r = mem.load_u32(KALE_RESP_ADDR);
r != 0 && r == kale_response(kale_challenge(self.tick))
}NARCISSUS#27 · REINT · HRV · full-meter 1.9
What it checks. NARCISSUS requires the bot to read much of its original code region. Its response must also match a digest of the immutable uploaded image.
| VM note | What it means |
|---|---|
read_own_code_cov | Coverage of original code-region bytes read as data. |
Build and activate it
- Sweep the original code region with word
LOADs during the current tick. Coverage counts distinct read bytes below0x2000; it must reach at least 75% ofmin(code_end,0x2000). - Start the digest with the current challenge. For addresses
0,4,8,… < code_end, foldh = econ_hash(h XOR original_image_word_at_a). Write the final digest to the response cell. The host hashes the original image. Preserve or reconstruct original bytes if the bot changed its live image.
Proof for this tick
0xFFD80x4E415243 → 0xFFE00xFFDC0x4E617231That constant is not used by this digest check; the response is the image digest described above.
Read this chain’s exact host check
fn narcissus_armed(&self, mem: &Mem, _l: PwnLatches) -> bool {
let image = match &self.bot_image {
Some(img) => img,
None => return false,
};
let cov_target = self.bot_code_end.min(FRAME_BASE);
if cov_target == 0
|| (_l.read_own_code_cov as u64) * 4 < (cov_target as u64) * 3
|| mem.load_u32(NARC_MAGIC_ADDR) != NARC_MAGIC
{
return false;
}
let want = self_hash_digest(image, 0, self.bot_code_end, narc_challenge(self.tick));
let r = mem.load_u32(NARC_RESP_ADDR);
r != 0 && r == want
}YGGDRASIL#28 · REINT · THR · full-meter 1.7
What it checks. YGGDRASIL checks a digest of one image segment selected by the challenge. A code-region load must occur, but the host does not tie that load to the selected segment.
| VM note | What it means |
|---|---|
read_own_code | The program read bytes from its own code region as data. |
Build and activate it
- Read the current challenge; choose one of eight image segments with
seg = challenge % 8,lo = floor(seg·code_end/8), andhi = floor((seg+1)·code_end/8). - Starting with
h=challenge, walka=lo,lo+4,… < hiand foldh=econ_hash(h XOR original_image_word_at_a). Put the digest in YGGDRASIL’s response cell. As with NARCISSUS, the host compares against original image bytes, not rewritten live memory. - The checker requires a code-region
LOADduring this tick. It does not require a load from the selected segment. An unrelated code load satisfies that note. The digest must still match the selected original-image segment and fresh challenge.
Proof for this tick
0xFFE40x59474744 → 0xFFEC0xFFE80x59676731That constant is not used by this digest check; the response is the selected-segment image digest described above.
Read this chain’s exact host check
fn yggdrasil_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
let image = match &self.bot_image {
Some(img) => img,
None => return false,
};
if !l.read_own_code || self.bot_code_end == 0 || mem.load_u32(YGGD_MAGIC_ADDR) != YGGD_MAGIC
{
return false;
}
let chal = ygg_challenge(self.tick);
let (lo, hi) = ygg_segment(chal, self.bot_code_end);
let want = self_hash_digest(image, lo, hi, chal);
let r = mem.load_u32(YGGD_RESP_ADDR);
r != 0 && r == want
}GLYPH#29 · REINT · HRV · full-meter 1.7
What it checks. GLYPH uses the VM’s value-origin tracking. The bot creates clean and tainted stack slots to encode a response from the current challenge.
| VM note | What it means |
|---|---|
g_word | A word that records where the value came from along a defined data path. |
Build and activate it
- Read this tick’s challenge and calculate
econ_hash(challenge XOR GLYP_ECHO). Move the stack to the 32-slot window at0xD800. For each bit 0–31, useLOADfor a 1 orPUSH_ZEROfor a 0. - The VM packs those slot taint bits into
g_wordafter execution. Write the magic before painting and avoid later stack activity that overwrites those slots. The packed taint pattern is the response; there is no response cell.
Proof for this tick
0xFFF00x474C5950 → 0xFFF40x476C7931No response cell: the 32 taint slots carry the ECHO-hash response.
Read this chain’s exact host check
fn glyph_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if l.g_word == 0 || mem.load_u32(GLYP_MAGIC_ADDR) != GLYP_MAGIC {
return false;
}
l.g_word == glyph_response(glyph_challenge(self.tick))
}DOPPELROLL#30 · REINT · HRV · full-meter 1.75
What it checks. DOPPELROLL requires overlapping guest-memory output ranges from enemy and gem scans, so both scans must return records and write bytes. It watches that collision between writers; it does not mean the bot changed native host memory.
| host observation | What it means |
|---|---|
writer_collision (host) | The host observed overlapping guest-output write ranges in one tick. |
Build and activate it
- Call
scan_enemiesandscan_gemswith overlapping output buffers in one tick. The sameresult_addris the simplest choice. Both scans must return records. First kill an enemy to create a gem while another enemy remains. - The host records enemy writes as
[addr,addr+16·n)and gem writes as[addr,addr+12·n). Their half-open ranges must share a byte; touching only at an endpoint, or an empty scan, does not setwriter_collision.
Proof for this tick
0xFE000x444F5050 → 0xFE080xFE040x446F7031Read this chain’s exact host check
fn doppelroll_armed(&self, mem: &Mem, _l: PwnLatches) -> bool {
if !self.writer_collision || mem.load_u32(DOPP_MAGIC_ADDR) != DOPP_MAGIC {
return false;
}
let r = mem.load_u32(DOPP_RESP_ADDR);
r != 0 && r == dopp_response(dopp_challenge(self.tick))
}SPHINX#31 · REINT · THR · full-meter 2.0
What it checks. SPHINX combines a code-region read, a computed branch target, and four destinations from one dispatch site. The host checks all three observations.
| VM note | What it means |
|---|---|
computed_jump | The branch destination came from a computed value, not only a compiler-emitted literal. |
dispatch_max | Maximum distinct targets seen from one dispatch site in the tick. |
read_own_code | The program read bytes from its own code region as data. |
How the fields relate: Code-as-data reading, computed destination origin, and sufficient breadth from one dispatch site must be understood together for SPHINX's own response check. Merely reading code, or merely branching to several locations, leaves the other relationship unproved. Its meter is separate from the related CLASSIC chains.
Build and activate it
- LOAD at least one word from the original code region so
read_own_codeis set. Build a shared-site dispatcher with four distinct destinations in the same tick. - Make at least one
JMP_ABS/CALL_ABSdestination at that dispatcher come fromLOADor tainted arithmetic. The host joins own-code read, computed target, and four-target breadth; they do not have to come from one literal instruction.
Proof for this tick
0xFE0C0x53504858 → 0xFE140xFE100x53706831Read this chain’s exact host check
fn sphinx_armed(&self, mem: &Mem, l: PwnLatches) -> bool {
if !(l.read_own_code && l.computed_jump && l.dispatch_max >= SPHINX_DISPATCH_MIN)
|| mem.load_u32(SPHX_MAGIC_ADDR) != SPHX_MAGIC
{
return false;
}
let r = mem.load_u32(SPHX_RESP_ADDR);
r != 0 && r == sphx_response(sphx_challenge(self.tick))
}The frozen CLASSIC apex
bots/reference/classic_apex keeps two recovered competitor uploads. The main apex file is 53,264 bytes and the earlier one is 21,066 bytes. The README says the main file covers eight chains across four families. Its middle score was 224,384 before the balance change and 22,704 afterward.
Keeping the exact file lets the authors measure how a reward change affects a real bot. The older file gives another comparison point. The reference-bot comparison explains the added LOCKSMITH chain. Because the uploaded bytes stay fixed and only the reward rules change, the score difference tells us about the rules' effect.
The frozen CLASSIC apex is the strongest preserved CLASSIC reference in this guide. Its measurements show what those bytes achieved, but they do not prove that no bot can score higher.
Why CLASSIC can look like the whole challenge
CLASSIC has enough chains and strong enough rewards to feel like the whole challenge. In playtests, some agents stayed in CLASSIC and never found the device route, and one even decided that it had finished. The lesson is simple: explaining what you have found does not prove there is nothing else. The next chapter shows where ABYSS lives in the running code.
Gem · 3 EXP