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.
The bot puts a frame and input bytes in its 64 KiB guest memory. It keeps their address for the call.
GATECALL pops that address and passes the remaining tick budget with guest memory.
The host checks the address and frame shape, then copies the frame and input. Later bot writes cannot change this saved request.
It checks the request kind, session, work limits, and resource rights. It can answer early without running a cell.
If work is admitted, a cell can change a Gate register. Our later toy cell loads 7 into r2; that is just a calculation.
r20→7This 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.
0x0100–0x017fGATECALL receives 0x0100. The frame names the operation and points to the two areas below.
0x0300–0x031fThe frame says input pointer 0x0300, length 32. More commands use more 32-byte slots, up to 256 bytes.
0x0400–0x0407The 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 bytesThese 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:
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
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
Only the later game step turns accepted proof into a bounded boost. The executor never gets an EXP-edit button.
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:
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:
struct CpuLane {
registers: [u32; REGISTER_COUNT],
condition: bool,
thread_cursor: u16,
dispatch: FixedStack,
returns: FixedStack,
cursors: FixedStack,
}| 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
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:
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:
- Pushing 9 checks room, writes
words[0] = 9, then changesspfrom 0 to 1. - Pushing 4 writes the next slot and changes
spto 2. - Popping decrements
spto 1, reads 4, clears that slot, and returns 4. - A later pop from
sp = 0returnsMachineFault::Stackbefore 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.
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:
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:
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 |
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:
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; event.result = LaneWords::from_array(results);
self.record_event(event);
self.apply_successors(&successors)?;
Ok(StepEffect::Continue)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:
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.
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.
512 − 300 used by Gate work today. Refills next tick.
A fresh call can use at most 256. Refreshes on another call.
512 − 180 used across its life. Never refills for this program.
min(212, 256, 332) = 212 steps120 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:
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:
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:
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:
) -> 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.
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.
3 + 5 + 7 + 11 + 13 + 17565 + 7 + 11 + 13 + 17533 + 7 + 11 + 13 + 17513 + 5 + 11 + 13 + 17493 + 5 + 7 + 13 + 17453 + 5 + 7 + 11 + 17433 + 5 + 7 + 11 + 1339A 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:
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:
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,
};
}Start with the live view. S0…S5 are the six host-made receipt words. The next table gives each one its check name.
owner word ⊕ S0 ⊕ S1 ⊕ S2 ⊕ S3 ⊕ S4First five sourcesearlier parent ⊕ request fields ⊕ S0…S4 ⊕ S5All six sourcesS0…S4Cancel that source in root and parent. The other five sources stay in place.
S5Cancel it in parent only. Root never contained source 5.
0101 ⊕ 0011 = 01100110 ⊕ 0011 = 0101XOR 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.
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:
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:
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.