EXPcaliburINSIDE THE MACHINE

09 / ABYSS / THE GATE

ABYSS engine: how the inner machine runs

The Gate is ABYSS's machine within the machine. Three parts share the work: the scheduler decides what may run, the executor does the math, and the host checks whether the result counts. A big number in a register is not a damage upgrade. The host wants receipts, meaning proof of the work behind the number.

Input: work the host has allowed, tied to a fixed owner and program.

Machine state: eight registers, three stacks, a cursor, work counters, and limited comparison runs.

Output: why execution stopped, how much work it used, and sometimes a proposed change. The host must still check and save that change.

First, follow one request into the Gate

The outer bot and the Gate run separate instruction streams. To use the Gate, the bot first writes a request into its own VM memory. Its program then runs GATECALL, which hands the host the request's address. The host reads and copies the request, checks that the work is allowed, and only then runs the Gate's eight-byte cells.

Think of handing in a form at a desk: the address tells the clerk where the form is, not what it says.

One request's trip: from bot memory to inner work and back
01 · BOT MEMORYPrepare the request

The bot puts a frame and input bytes in its 64 KiB guest memory. It keeps their address for the call.

02 · OUTER VMCall the host

GATECALL pops that address and passes the remaining tick budget with guest memory.

03 · HOST COPYFreeze what was sent

The host checks the address and frame shape, then copies the frame and input. Later bot writes cannot change this saved request.

04 · SCHEDULERDecide what may run

It checks the request kind, session, work limits, and resource rights. It can answer early without running a cell.

05 · INNER EXECUTORRun approved cells

If work is admitted, a cell can change a Gate register. Our later toy cell loads 7 into r2; that is just a calculation.

r207
06 · TWO THINGS TO CHECKReturn is not a reward
Bot receives a status word; the Gate's work also uses outer-VM time.Host may save proof only when the specific checker accepts the work.

This is an approved RUN-style path. HELLO, a bad request, or a busy scheduler can return before step 5. A changed register, a returned status, saved proof, an active boost, and EXP are five different events.

What is actually in that request?

The bot gives the outer VM one frame address. The 128-byte frame at that address holds the request type, session details, and a 64-byte body. Like a form with two address fields, it also points to two other areas. The input pointer says where to read commands; the output pointer says where to put a reply.

Three places in the outer bot's 64 KiB memory
FRAME · 128 BYTES0x0100–0x017f

GATECALL receives 0x0100. The frame names the operation and points to the two areas below.

INPUT · ONE 32-BYTE COMMAND0x0300–0x031f

The frame says input pointer 0x0300, length 32. More commands use more 32-byte slots, up to 256 bytes.

OUTPUT · EIGHT BYTES OF ROOM0x0400–0x0407

The frame says output pointer 0x0400, capacity 8. This is room for a reply, not extra input.

Inside the 128-byte frame Offsets below are from 0x0100, the example frame address.

+0x00–0x17Header and identityBRD1, version, RUN/HELLO, sequence, nonce, session and lineage+0x18–0x5764-byte bodyFour 16-byte tiles used by the surface checks+0x58–0x5fTwo address pairsInput pointer + length; output pointer + capacity+0x60–0x7fMore contextFibre handle, epoch, request class, challenge bank, and reserved bytes

These addresses are a teaching example, not a complete proof payload. The host checks that all three ranges fit in the supplied guest memory. It then saves the frame and input bytes in its own OwnedInvocation. It checks the output range but does not copy its old contents. Later writes by the bot cannot edit the saved request.

That gives the first useful split. A pointer only tells the host where to look; the copied bytes tell it what was requested. A valid address and frame shape can still fail the later session, permission, and proof checks. The surface map works through them one by one.

Now follow the result back out. In the outer VM, packed carries both a status and a work count. The bot receives only the low status bits on its stack. The VM charges the bounded work count to this tick's budget instead of giving it to the bot:

RUST crates/vm/src/lib.rs · source lines 1259–1265
                    let packed = host.gatecall(req_ptr, budget, &mut self.mem);
                    let status = packed & GATE_STATUS_MASK;
                    let steps = (packed >> GATE_STEPS_SHIFT).min(GATE_STEPS_MAX);
                    self.push_tagged(status, true);
                    executed = executed
                        .saturating_add(GATECALL_BUDGET_COST)
                        .saturating_add(steps);

The host copies the request before the scheduler uses it. From then on, the copy is frozen: later bot writes cannot change it. The exact address and copy checks are below; the rest of this chapter starts after that boundary:

See how the host freezes the request
RUST crates/gate/src/abyss/wire.rs · source lines 327–357
impl OwnedInvocation {
    pub fn copy_from_guest(guest: &[u8], request_pointer: u16) -> Result<Self, StructuralError> {
        let frame = copy_guest_frame(guest, request_pointer)?;
        let decoded = BraidFrame::decode(&frame)?;
        let descriptor = decoded.input_descriptor();
        let range = descriptor
            .checked_range()
            .ok_or(StructuralError::InputRange)?;
        let source = guest
            .get(range.as_range())
            .ok_or(StructuralError::InputRange)?;

        let output = decoded
            .output_descriptor()
            .checked_range()
            .ok_or(StructuralError::OutputRange)?;
        if guest.get(output.as_range()).is_none() {
            return Err(StructuralError::OutputRange);
        }

        let mut input = [0u8; INPUT_MAX];
        input[..source.len()].copy_from_slice(source);
        Ok(Self {
            frame,
            input,
            input_len: descriptor.length,
        })
    }

Start with the ownership boundary

Keep calculation and acceptance separate. The inner machine can calculate a value, but the host decides whether it counts. A register holding 12 stays just a register holding 12 until the host accepts the work behind it.

Permission, computation, receipt

SchedulerChecks the owner, resource, queue, and allowed work.
ExecutorRuns the inner cells under work limits and returns what happened.
Host checkerTests the result and may save a proof receipt.

Only the later game step turns accepted proof into a bounded boost. The executor never gets an EXP-edit button.

The scheduler grants permission; the executor runs the workThe scheduler grants permission; the executor runs the workScheduler ownsSessions and version numbersHandles and permissionsWaiting workSaved proofExecutor ownsCopied programRegisters and stacksWork budget and pause pointLive and comparison runsstart / validated resumeyield or resultGate RAMLive data within the allowedcontext. It is not the outer VMmemory.Host checkThe machine proposes a result; the hostdecides whether to save proof.bounded data accesscandidate
The scheduler grants permission; the executor does the calculation. The scheduler fixes who and what may run. The executor proposes a result. Host checks decide whether to save proof of it. View diagram at full size

The scheduler hands the executor a fixed starting package. It holds the run's identity, the comparison views, and a table of approved paired branches. Comparison runs may briefly take different paths only at those branches. The executor cannot invent these values itself:

RUST crates/gate/src/abyss/threaded.rs · source lines 446–450
pub(super) struct StartConfig {
    pub identity: FrozenIdentity,
    pub views: CounterfactualViews,
    pub diamonds: DiamondTable,
}
Owner State it controls Consequence
Scheduler Sessions, resource permissions, queue, proof store, logs Can allow, reject, and save work
Executor Registers, stacks, copied instructions, paused work, comparison runs Can calculate within the allowed request
World Knight, enemies, combat, gems, EXP Uses limited boosts after proof is checked

Paused work stores values and indices, not pointers to host objects. Its identity is frozen in the same way, fixed when the host admitted the work. Later guest writes cannot change that FrozenIdentity, so resumed work keeps the admitted owner and target. The copied instructions never change either.

A machine is state plus transition rules

Start with what the Gate can remember. Each instruction reads some of these fields and changes others. Name the fields, and you can follow any instruction trace.

Here is one execution lane in the source:

RUST crates/gate/src/abyss/threaded.rs · source lines 1034–1041
struct CpuLane {
    registers: [u32; REGISTER_COUNT],
    condition: bool,
    thread_cursor: u16,
    dispatch: FixedStack,
    returns: FixedStack,
    cursors: FixedStack,
}
One inner CPU lane: values, routing, and saved positionsOne inner CPU lane: values, routing, and saved positionsRegister fileEight 32-bit words carry numeric values.A number is not an access permission.Condition + thread cursorThe comparison result and current threadedposition travel with the continuation.Dispatch stackSelects threaded work.64 entries; each entryis u16.Return stackSaves call continuations.64 entries; each entryis u16.Cursor stackSaves traversal positions.64 entries; each entryis u16.Peer fields inside one CpuLane · no permission flows from a register into a stackAll fields belong to a bounded lane. The executor maintains the live lane plus six comparison lanes.The scheduler separately owns capabilities, sessions, queued work, and persistent acceptance.
Open the executor: each lane has a small explicit state. Registers hold values; three stacks preserve different kinds of positions; condition and cursor control progress. Capability permission belongs to the scheduler outside this lane. View diagram at full size
Field Representation Job
registers Eight u32 words Arithmetic values and instruction operands
condition Boolean Saved comparison result
thread_cursor u16 Current position in threaded execution
dispatch Limited stack Which work runs next
returns Limited stack Where to return after a call
cursors Limited stack Saved positions while walking data

A register holding an address still holds just a number. That number grants no permission. The scheduler checks the current context and capability before any resource is used.

Fixed dimensions: what fits, and what does not

RUST crates/gate/src/abyss/threaded.rs · source lines 13–21
pub(super) const GATE_RAM_BYTES: usize = 8 * 1024;
pub(super) const PAGE_BYTES: usize = 1024;
pub(super) const CELL_BYTES: usize = 8;
pub(super) const CELLS_PER_PAGE: usize = PAGE_BYTES / CELL_BYTES;
pub(super) const STACK_CAPACITY: usize = 64;
pub(super) const REGISTER_COUNT: usize = 8;
pub(super) const COUNTERFACTUAL_LANES: usize = 6;
pub(super) const MAX_FIBRE_STEPS: u16 = 512;
pub(super) const MAX_CALL_FUEL: u16 = 256;
Resource Bound Derived meaning
Gate RAM 8,192 bytes Eight pages of 1,024 bytes
Cell 8 bytes 128 cells fit in a page
Registers 8 words Program length does not enlarge the register file
Each of the three stacks 64 entries Full and empty are explicit machine states
Comparison runs 6 plus live Extra checks with changed inputs; the bot cannot read their results
Per-call fuel 256 accounting steps Limits one invocation
Fibre lifetime 512 steps Limits the whole continued computation

The scheduler has its own limits: 32 capability slots, 16 queue entries, and 64 proof nodes. Each limit covers its own kind of state. An empty stack slot cannot help when the queue is full.

Why three stacks?

Why not one stack? The Gate tracks three different things: which work runs next, where a call returns, and which data position to restore. Each stack keeps the answers to one question, so the answers never mix.

Operation concept State it concerns Question answered later
Select threaded work Dispatch stack Which work should execute?
Call a routine Return stack Where should execution resume?
Save traversal position Cursor stack Which position should be restored?

All three share one fixed-size storage code. Each stack holds u16 values and a u8 index for its next free slot. Here are the push and pop operations:

RUST crates/gate/src/abyss/threaded.rs · source lines 1002–1020
    fn push(&mut self, value: u16) -> Result<(), MachineFault> {
        if !self.can_push(1) {
            return Err(MachineFault::Stack);
        }
        self.words[self.len()] = value;
        self.sp += 1;
        Ok(())
    }

    fn pop(&mut self) -> Result<u16, MachineFault> {
        if self.sp == 0 {
            return Err(MachineFault::Stack);
        }
        self.sp -= 1;
        let index = self.len();
        let value = self.words[index];
        self.words[index] = 0;
        Ok(value)
    }

Trace the state changes

For an initially empty stack:

  1. Pushing 9 checks room, writes words[0] = 9, then changes sp from 0 to 1.
  2. Pushing 4 writes the next slot and changes sp to 2.
  3. Popping decrements sp to 1, reads 4, clears that slot, and returns 4.
  4. A later pop from sp = 0 returns MachineFault::Stack before decrementing anything.

The bounds check happens before the stack changes. A pop also clears the slot it leaves, so unused slots never hold stale values. All three stacks share these mechanics but hold different kinds of positions.

Cells: the inner machine's instruction unit

There are two machines here. The outer VM runs the bot you uploaded, while the Gate runs a different program submitted through ABYSS requests. Outer instructions vary in length and mostly work on a stack. Every Gate instruction, called a cell, is exactly eight bytes and mostly works on registers. So don't read a Gate cell as an outer-VM opcode.

Property Outer VM Inner Gate
Instruction representation Opcode stream with instruction-specific immediates Eight-byte cells
Main numeric state Operand stack in guest memory Eight 32-bit registers per lane
Control state Instruction pointer and software call conventions Thread cursor plus dispatch and return stacks
How it names an operation Outer opcode A number looked up in this run's operation table
Work limit Per-tick outer budget Per-call and whole-paused-run inner limits

Each cell has three jobs: it names an operation, says where the result goes, and carries a number. Lit makes an easy first toy cell: it just puts a number in a register. It does not move the knight or earn proof by itself.

Assume the host has already allowed this program page. The first two bytes are a lookup number into this run's operation table, so no fixed pair of bytes always means Lit. The next two bytes hold settings, and the last four hold the number to load, lowest byte first.

Select each part of the cell below. The two ·· tiles are blank because their values depend on the run.

Read one Gate instruction

A teaching Lit 7 → r2 cell. The two blank tiles are the run-selected operation number; the other six bytes are concrete. Select a step to see how the machine reads these bytes.

Run's operation lookupSettingsNumber to load

This is one inner instruction in ABYSS proof work. A changed register does not earn proof by itself. Later, host checks decide whether to save a receipt. The game decides whether the knight collects EXP.

The decoder reads those three parts in order. Here, the settings bytes 82 00 select destination register 2, no source register, 32-bit width, and mode zero. The number bytes 07 00 00 00 hold 7. But the first two bytes must select Lit in this run's table. If they name another operation, the cell is not Lit; if they name none, the Gate rejects the cell. Here is the code that unpacks the bytes:

RUST crates/gate/src/abyss/threaded.rs · source lines 219–234
pub(super) fn decode_cell(
    bytes: &[u8; CELL_BYTES],
    relocation: &RelocationTable,
) -> Result<DecodedCell, DecodeFault> {
    let physical_stub = u16::from_le_bytes([bytes[0], bytes[1]]);
    let spec = u16::from_le_bytes([bytes[2], bytes[3]]);
    let immediate = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
    let logical_result = logical_for_physical(relocation, physical_stub);
    let logical = logical_result.unwrap_or(0);
    let primitive_result = Primitive::from_logical(logical);
    let primitive = primitive_result.unwrap_or(Primitive::Nop);
    let dst = (spec & 7) as u8;
    let src = ((spec >> 3) & 7) as u8;
    let width_bits = ((spec >> 6) & 3) as u8;
    let mode = (spec >> 8) as u8;
    let width_result = CellWidth::from_bits(width_bits);
Byte positions Decoded role Example meaning for a benign literal
0–1 Operation number for this run Lookup gives Lit
2–3 Packed spec Destination register 2, 32-bit width; unused source and mode zero
4–7 Little-endian immediate The word 7

Follow one step. Suppose r2 = 0 and the toy cell says Lit 7 → r2; after it runs, r2 = 7. The Gate notes that the 7 came from a literal, then counts the work and moves to the next checked cell. It does this in the live run and in all six comparison runs. Here is the Rust branch that starts that work:

RUST crates/gate/src/abyss/threaded.rs · source lines 2349–2357
            Primitive::Lit => {
                let successors = resolve_checked(self.ordinary_successors(fetched))?;
                let mut results = [0u32; LANE_COUNT];
                for lane_index in 0..LANE_COUNT {
                    let cell = fetched.cells[lane_index];
                    self.lane_mut(lane_index).registers[usize::from(cell.dst)] = cell.immediate;
                    results[lane_index] = cell.immediate;
                }
                let dst = usize::from(fetched.cells[0].dst);

This toy cell shows only that a literal was loaded. It does not show that the program used a challenge input. It earns no module receipt either. A run can also stop early: Yield can pause it with work left, and a fault ends it. Even a proposed final result must still pass a separate host check.

Let four harmless cells run together

Now run four cells in a row. Watch three things: register values, the next cell's address, and work used. Start with r2 = 0 and r3 = 0, and call the first cell's address A. Again, assume the host has allowed this page. This is ordinary arithmetic, not a proof program.

Try the four steps below before reading the full table. The last button starts a new allowed call, not a fifth cell. That call resets the per-call counter but keeps the paused program's registers, cursor, and lifetime work count. So a new call does not restart the old work.

Step through four Gate cells

Tap one cell at a time. Watch the registers, next-cell address, and two work counters. This follows the live lane only; the six comparison lanes and proof checks happen outside this teaching trace.

Swipe to follow the cursor and work count →

Cell at Job r2 after r3 after Next cursor Work used in this call
A Lit: r2 ← 7 7 0 A + 8 2
A + 8 Lit: r3 ← 5 7 5 A + 16 4
A + 16 Alu: r2 ← r2 + r3 12 5 A + 24 6
A + 24 Yield: pause 12 5 A + 32 7
Four Gate cells: watch value, cursor, and workFour Gate cells: watch value, cursor, and workCELLREGISTERS AFTERNEXT CURSORWORKALit r2 ← 7r2: 7 r3: 0A + 82A + 8Lit r3 ← 5r2: 7 r3: 5A + 164A + 16Alu: addr2: 12 r3: 5A + 246A + 24Yieldr2: 12 r3: 5A + 327Yield saves the machine at A + 32. A paused result is not accepted proof.
Four small cells, three things to track. Read each row as one step: the register values after it, the cursor for the next cell, and total work so far. The final cell pauses; it does not grant proof. View diagram at full size

Each cell is eight bytes long, so moving to the next cell adds eight to the cursor. In the Alu cell, dst = 2, src = 3, and mode 0 selects addition. The Gate reads 7 from r2 and 5 from r3, then writes 12 back to r2.

Count the work and read the result

The four cells use 2, 2, 2, and 1 work units. The table shows only the live run, but six comparison runs also execute, each with its own register values.

Here is the interpreter's arithmetic. The Alu arm reads its inputs, writes the result, and moves to the next cell. Add mode uses 32-bit wrapping arithmetic:

RUST crates/gate/src/abyss/threaded.rs · source lines 2380–2395
                for lane_index in 0..LANE_COUNT {
                    let cell = fetched.cells[lane_index];
                    let lane = self.lane(lane_index);
                    let left = lane.registers[usize::from(cell.dst)];
                    let right = if cell.mode & 0x10 != 0 {
                        cell.immediate
                    } else {
                        lane.registers[usize::from(cell.src)]
                    };
                    lefts[lane_index] = left;
                    rights[lane_index] = right;
                    results[lane_index] = alu_result(cell.mode & 7, left, right);
                }
                for (lane_index, value) in results.iter().copied().enumerate() {
                    let dst = usize::from(fetched.cells[lane_index].dst);
                    self.lane_mut(lane_index).registers[dst] = value;
RUST crates/gate/src/abyss/threaded.rs · source lines 2409–2412
                event.result = LaneWords::from_array(results);
                self.record_event(event);
                self.apply_successors(&successors)?;
                Ok(StepEffect::Continue)
RUST crates/gate/src/abyss/threaded.rs · source lines 2901–2905
fn alu_result(mode: u8, left: u32, right: u32) -> u32 {
    match mode {
        0 => left.wrapping_add(right),
        1 => left.wrapping_sub(right),
        2 => left ^ right,

The Yield cell moves the cursor to the next valid cell before returning a paused result:

RUST crates/gate/src/abyss/threaded.rs · source lines 2577–2585
            Primitive::Yield => {
                let successors = resolve_checked(self.ordinary_successors(fetched))?;
                self.coverage.yields = self.coverage.yields.saturating_add(1);
                self.record_event(SemanticEvent::for_cell(
                    SemanticEventKind::Yield,
                    fetched.cells[0],
                ));
                self.apply_successors(&successors)?;
                Ok(StepEffect::Yield)

Swipe to see the saved work count →

Moment Machine state Next cursor r2 Work used across this paused run
After Yield returns Paused A + 32 12 7
Start of the next allowed call Running again A + 32 12 Still 7; this call starts its own counter at 0

The host receives a paused result. r2 = 12 is just saved calculation, not a sword upgrade. If the host allows another call, the Gate resumes at A + 32, still holding r2 = 12 with seven work units already spent. Even a final proposed result needs a host check before proof is saved. The next section shows that second call's work limit.

Beyond arithmetic, the Gate can compare values, access memory, and move its cursor. It can also call and return, use handles, pause, and propose a result. Through all of this, keep two numbers apart. The cursor says where to read the next cell, while the operation number says what that cell does.

Fuel: which limit stops the next call?

Why count work? One outer request must not make the inner machine run forever. Think of the Gate's work limits as three partly used batteries. The next call can spend only what the smallest battery has left.

The first is a 512-step pool per game tick, shared by all Gate work in that tick. The second is the call: at most 256 steps per inner-machine call. The third belongs to the paused program: at most 512 steps in its whole life, even across ticks. Operations also have different costs, so one eight-byte cell may use more than one step.

Three batteries, one call: the smallest remaining number wins

Suppose this paused program already used 180 steps in an earlier call. Other Gate work has used 300 steps during the current tick. The host allows this program to resume. Every bar below uses the same 512-step scale.

THIS GAME TICK212 left

512 − 300 used by Gate work today. Refills next tick.

THIS NEW CALL256 available

A fresh call can use at most 256. Refreshes on another call.

THIS PAUSED PROGRAM332 left

512 − 180 used across its life. Never refills for this program.

Maximum this callmin(212, 256, 332) = 212 steps
If it uses all 212The program has 120 lifetime steps left. Next game tick, Gate's tick pool resets to 512 and another call starts with 256—but this same program can now use only min(512, 256, 120) = 120.

Here the Gate works out this tick's remaining pool and passes it to the saved machine. This code sets only a work limit, so the request still needs an allowed execution context:

RUST crates/gate/src/abyss/state.rs · source lines 2609–2627
    fn run_threaded_machine(&mut self) -> DriveAttempt {
        let available = GATE_FIBRE_STEP_LIMIT.saturating_sub(self.gate_steps_this_tick);
        let result = {
            let Some(context) = self.threaded_context.as_mut() else {
                self.threaded.reset();
                return DriveAttempt {
                    valid: false,
                    semantic_steps: 0,
                    terminal_work_done: false,
                };
            };
            let grant = context.grant;
            let threaded = &mut self.threaded;
            let verifier = &mut context.verifier;
            self.capabilities.with_execution_ram(grant, |ram| {
                threaded.run_call(ram, available.min(MAX_CALL_FUEL), verifier)
            })
        };
        let result = match result {

The inner machine then takes the smallest of the three: tick remainder, 256-step call limit, and lifetime remainder. If its lifetime is already spent, it faults instead of starting another call:

RUST crates/gate/src/abyss/threaded.rs · source lines 1883–1900
        if self.state == MachineState::Yielded {
            self.state = MachineState::Running;
            self.coverage.crossed_execution_call = true;
        }

        if self.semantic_steps_total >= MAX_FIBRE_STEPS {
            self.reset();
            return CallResult {
                semantic_steps: 0,
                stop: CallStop::Faulted(MachineFault::TotalFuel),
            };
        }

        let lifetime_remaining = MAX_FIBRE_STEPS - self.semantic_steps_total;
        let allowance = available_tick_fuel
            .min(MAX_CALL_FUEL)
            .min(lifetime_remaining);
        let mut used = 0u16;

The outer VM keeps a separate allowance of 10,000 instruction units per game tick. Each GATECALL adds a fixed 64, plus the Gate steps it used, to that outer count. That count is not one of the Gate's three batteries. A new tick refills both the outer allowance and the Gate's tick pool. But it never refills a paused program's lifetime.

Which bytes are frozen, and which remain live?

What stays fixed? A paused, approved program must resume with the same instruction page. But it may still read or update any data it has permission to use.

State Treatment Why
Execution page Immutable snapshot Keeps the admitted instruction representation stable
Frozen identity Preserved context Connects resumed work to the admitted owner and target
Permitted Gate data Live state Allows computation to use current authorized data
Capability/resource table Revalidated at use Resource lifetime can change while work is pending

Copying the instructions does not freeze every object. When work resumes, the scheduler must still decide whether each resumed reference identifies the same current authorized object.

Capability validation: unpack every check

A capability is a handle with permissions. A table slot number alone cannot identify the object. It also cannot say how the object may be used.

The normal page validator checks the complete current relationship:

RUST crates/gate/src/abyss/capability.rs · source lines 2191–2213
    fn validate_normal(
        &self,
        handle: CapHandle,
        session: SessionIdentity,
        required_kind: Option<CapKind>,
        required_rights: u8,
    ) -> Option<CapSlot> {
        if !session.is_valid() || usize::from(handle.slot) >= ALLOCATABLE_CAP_SLOTS {
            return None;
        }
        let slot = self.slots[usize::from(handle.slot)];
        let page_index = usize::from(slot.physical_page);
        let kind_valid = required_kind.is_none_or(|required| slot.kind == required.as_u8());
        (slot.is_live_page()
            && slot.generation == handle.generation
            && kind_valid
            && slot.rights & required_rights == required_rights
            && slot.lineage == session.lineage
            && slot.owner_session_slot == session.slot
            && page_index < PAGE_COUNT
            && self.page_owner[page_index] == handle.slot)
            .then_some(slot)
    }
Check in the excerpt Assumption it protects
Valid session There is a current owner context
Capability slot in range The table lookup stays inside the table
Live page The resource still exists
Generation equals reference generation A reused slot has not substituted a new occupant
Current kind and required kind agree The object is still the expected resource type
Required rights are present The requested use is permitted
Lineage and session owner agree Another context's resource is not being borrowed
Page index and reverse owner map agree Forward and reverse ownership records describe the same page

What makes the retained dispatch path special

Suppose slot 5 held page A and later holds page B. An old ordinary handle for A now fails, because matching the slot number is not enough. A saved dispatch goes through a separate check instead. It compares the saved and current identities, owner, page, and permissions.

The host checks again before it copies the program page. The queue timeline shows what changes between saving the work and resuming it.

The code has names for these two routes. A current program page uses ProgramPage; a saved dispatch that passes its own check uses ExecutionSource::StaleDispatch:

RUST crates/gate/src/abyss/capability.rs · source lines 2062–2081
    ) -> Option<ExecutionGrant> {
        if let Some(slot) =
            self.validate_normal(handle, session, Some(CapKind::ProgramPage), RIGHT_EXECUTE)
        {
            return Some(ExecutionGrant {
                handle,
                session,
                physical_page: slot.physical_page,
                source: ExecutionSource::ProgramPage,
            });
        }
        let cap = self.validate_stale(handle, session)?;
        (cap.rights & RIGHT_EXECUTE != 0).then_some(ExecutionGrant {
            handle,
            session,
            physical_page: cap.physical_page,
            source: ExecutionSource::StaleDispatch,
        })
    }

The scheduler remembers which route passed. For the saved route, it uses the StaleBootstrap checker. That checker also needs earlier accepted cost work and a record of the resource reuse. The bot cannot make an old handle valid just by claiming that it is.

All of this concerns Gate objects that the host manages, and proof of what the inner program did as they were reused. None of it is native host-memory corruption.

Comparison runs: did the input matter?

The problem: a program can return the right value without doing the required work. Compare return x + 3 with return 10: both return 10 when x = 7. If x = 8, though, only the first result follows the input. Comparison runs test exactly that.

Did the program use the input?

Both tiny programs return 10 when x is 7. Change x to see which answer follows it. This is a small teaching example; the real Gate checks more.

Watch the same program get seven chances to give itself away

The Gate runs the same inner program in one live lane and six private comparison lanes. Each comparison lane sets one check's contribution to zero. If that check never passed, its contribution was zero already. The bot cannot read the private results or use their memory as spare input.

Start with toy lanes and toy numbers. Suppose six accepted checks contribute 3, 5, 7, 11, 13, and 17, and a toy program adds them. The real version is more complex. The host makes 32-bit words from accepted receipts, and the real module does more than add.

Practice run: remove one number at a time

The live lane sees every number. Each copy removes one number and runs the same addition again. The changed answer tells us that the program used that number.

LIVE · all six3 + 5 + 7 + 11 + 13 + 1756
COPY 0 · remove 35 + 7 + 11 + 13 + 1753
COPY 1 · remove 53 + 7 + 11 + 13 + 1751
COPY 2 · remove 73 + 5 + 11 + 13 + 1749
COPY 3 · remove 113 + 5 + 7 + 13 + 1745
COPY 4 · remove 133 + 5 + 7 + 11 + 1743
COPY 5 · remove 173 + 5 + 7 + 11 + 1339

A program that simply prints 56 prints 56 in every lane. If the sixth check was never earned, its live contribution is already zero. Then live and copy 5 both give 39 in this toy. The host still checks the exact expected answers and the work trace; different numbers alone do not earn proof.

Now replace the toy numbers with the real inputs. The host turns each accepted receipt into a 32-bit source word. A missing receipt gives zero. The Gate then provides three host-filled inputs: a root, a parent, and a pulse with this tick's challenge.

The bot can use these inputs but cannot edit them like ordinary Gate RAM. Sources 0–4 enter both root and parent, while source 5 enters the parent only. That is why the last comparison copy changes only the parent, while copies 0–4 change both fields.

First, the host builds the parent's last word. It starts with earlier parent data and mixes in a four-byte request word. That word holds the three requested Drive levels, then bits marking which tracks have valid saved authority. Those bits are context, not a fourth power level.

The host also mixes in source words 0–4 by XOR. Source 5 comes a moment later. That means the parent already contains the first five sources before the next step:

RUST crates/gate/src/abyss/full.rs · source lines 318–330
pub(super) fn construction_parent_base(
    parent_words: [u32; 4],
    requested_sat: [u8; 3],
    authority_mask: u8,
    bindings: CertificateBindings,
) -> [u8; 16] {
    let sources = certificate_sources(bindings);
    let mut words = parent_words;
    words[3] ^= pack_request(requested_sat, authority_mask);
    for source in sources.iter().take(5) {
        words[3] ^= *source;
    }
    words_to_bytes(words)

Next, the host builds the live inputs. Root word 2 gets sources 0–4 by XOR. Parent word 3 gets source 5 on top of the parent base built above, and pulse gets the challenge.

Then come the six comparison copies, one for each source. Copies 0–4 each remove their source from both root and parent. Copy 5 removes source 5 from the parent only. The Rust loop shows both cases:

RUST crates/gate/src/abyss/full.rs · source lines 348–382
    let mut root_words = [
        u32::from_le_bytes(owner[0..4].try_into().unwrap_or([0; 4])),
        u32::from_le_bytes(owner[4..8].try_into().unwrap_or([0; 4])),
        u32::from_le_bytes(owner[8..12].try_into().unwrap_or([0; 4])),
        roots.relocation_words()[0],
    ];
    for source in sources.iter().take(5) {
        root_words[2] ^= *source;
    }
    let mut parent = parent_base;
    let mut parent_word_three = u32::from_le_bytes(parent[12..16].try_into().unwrap_or([0; 4]));
    parent_word_three ^= sources[5];
    parent[12..16].copy_from_slice(&parent_word_three.to_le_bytes());

    let mut views = CounterfactualViews::zero();
    views.actual = ClosedViews {
        root: words_to_bytes(root_words),
        pulse: challenge,
        parent,
    };
    for lane in 0..AUDIT_LANES {
        let mut lane_root = root_words;
        let mut lane_parent = parent;
        if lane < 5 {
            lane_root[2] ^= sources[lane];
        }
        let mut word_three = u32::from_le_bytes(lane_parent[12..16].try_into().unwrap_or([0; 4]));
        word_three ^= sources[lane];
        lane_parent[12..16].copy_from_slice(&word_three.to_le_bytes());
        views.lanes[lane] = ClosedViews {
            root: words_to_bytes(lane_root),
            pulse: challenge,
            parent: lane_parent,
        };
    }
Where the real Gate puts six receipt words

Start with the live view. S0…S5 are the six host-made receipt words. The next table gives each one its check name.

ROOT · WORD 2owner word ⊕ S0 ⊕ S1 ⊕ S2 ⊕ S3 ⊕ S4First five sources
PARENT · WORD 3earlier parent ⊕ request fields ⊕ S0…S4 ⊕ S5All six sources
PULSEthis tick's challengeIdentical in all seven runs
COMPARISON COPIES 0–4Remove one of S0…S4

Cancel that source in root and parent. The other five sources stay in place.

COMPARISON COPY 5Remove S5

Cancel it in parent only. Root never contained source 5.

WHY XOR CAN REMOVE A WORD0101 ⊕ 0011 = 01100110 ⊕ 0011 = 0101

XOR the same bits twice and you get the starting bits back. These four-bit values are a tiny example; the real receipt source is 32 bits. If a receipt is missing, its source is zero, so “remove zero” changes nothing.

The inner program runs once with the live inputs and once for each of the six changed copies. The host later compares all seven answers and the work trace. A matching final number alone is not proof.

Each comparison run starts with the same registers, stacks, and cursor as the live run. It also gets its own private copy of Gate RAM. A store changes only that private copy, never the live RAM or another run's RAM. Later loads can therefore follow a value through the program while the runs stay separate.

Follow one number through all seven runs

The six names on the board are host-accepted receipt categories, earned as one power track climbs through ABYSS. They are not six switches the bot can turn on:

Receipt What the host has checked First appears after
DualView The two Entry readings agree on the input Entry
EntryLoop The required Entry work actually ran Entry
CostTranscript The calibrated work used the expected step costs Rift
RootBinding That work belongs to this run's owner Rift
StaleDispatch The accepted path crossed a resource's old and new lifetimes Chasm
ModuleAudit The full module passed its extra audit Full module

The earlier receipts carry forward on the same power track. A missing sixth receipt does not erase the first five. It only means that the sixth source word is zero in this check.

The interactive board reuses the six toy numbers from the seven-row picture. Pick a case, then a comparison row. Gold marks the number removed from that copy. That lets you test a missing receipt on its own, without the real module's other checks.

Seven runs, one source held out at a time

These small numbers stand in for host-made receipt words. Choose a program, then tap any run to see its inputs and answer.

SELECTED RUN

Teaching arithmetic only. Real receipt words are derived by the host, and the full verifier checks much more than whether these answers differ.

With all six toy receipts present, the live result is 56. Removing the first receipt's 3 gives 53, and removing the sixth receipt's 17 gives 39. Every copy differs from the live result. A program that always returns 56, by contrast, gives 56 in every lane. Extra arithmetic that ignores the changed input does not help.

Now select “Sixth receipt missing.” Its source is already zero in the live run, so removing it again changes nothing. Comparison run 6 therefore matches the live result. That is how a five-kind Module proof can exist while its sixth kind is still unearned. The proof chapter shows why this limits usable power to level 12, not 16.

At Seal, the executor passes the live result and all six comparison results to the host. The code reads r0 from each lane separately. The host then checks those values against its own expected results and checks the operation trace too:

RUST crates/gate/src/abyss/threaded.rs · source lines 2253–2260
        let submission = SealSubmission {
            verifier: self.identity.verifier,
            actual_r0: self.actual.registers[0],
            counterfactual_r0: std::array::from_fn(|lane| self.counterfactual[lane].registers[0]),
            actual_condition: self.actual.condition,
            counterfactual_condition: std::array::from_fn(|lane| {
                self.counterfactual[lane].condition
            }),

Different answers alone do not pass. The verifier also checks the owner, program page, work count, required operations, final condition, and expected values. The comparison runs must follow the live run's checked control flow, except at the approved paired branches in the scheduler's starting package. Anywhere else, a branch that would split the runs faults instead of counting as clever proof. And a hundred steps that ignore the required source still ignore it.

The return value makes the final boundary explicit

When a call ends, the executor reports the work it spent and a stop reason:

RUST crates/gate/src/abyss/threaded.rs · source lines 954–966
pub(super) enum CallStop {
    NotActive,
    Yielded(YieldCause),
    Faulted(MachineFault),
    Halted,
    Committed(CommitCandidate),
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) struct CallResult {
    pub semantic_steps: u16,
    pub stop: CallStop,
}
Stop reason Meaning for the caller
NotActive There is no active inner work to execute
Yielded Control returns with an explicit yield cause
Faulted Execution encountered a machine failure
Halted Computation stopped; this alone grants no authority
Committed(candidate) A candidate crosses into the host's verification/commit handling

Despite its name, Committed does not let the executor write to the saved proof store. The scheduler and the host's checks still decide what counts. The next chapter shows how accepted work becomes a limited power level.