Ram Ra
RamRa Construction: Tracking Memory Accesses
What is RamRa?
RamRa (RAM Read Address) is a collection of
ram_d polynomials that track which memory address each
cycle accesses. Like BytecodeRa, we chunk addresses into
smaller pieces for efficient lookup proofs.
Structure:
RamRa = [
RamRa[0]: Vec<Option<u8>>, // First chunk, length = trace_length
RamRa[1]: Vec<Option<u8>>, // Second chunk, length = trace_length
...
RamRa[ram_d-1]: Vec<Option<u8>>, // Last chunk, length = trace_length
]
Key difference from BytecodeRa: Not every cycle
accesses memory! Cycles without RAM access store None.
The Construction Process
Input Data
1. RAMPreprocessing - Dense word storage from earlier:
RAMPreprocessing {
min_bytecode_address: 0x7fffa000, // Lowest memory address used
bytecode_words: [
0x0000000000000002, // 8 bytes at 0x7fffa000-0x7fffa007
...
]
}
2. Memory Layout - Defines valid memory regions:
MemoryLayout {
input_start: 0x7fffa000,
input_end: 0x7fffafff,
output_start: 0x7fffb000,
output_end: 0x7fffbfff,
...
}
3. Execution Trace - Cycles with memory accesses:
Cycle 0-7: No RAM access
Cycle 8: No RAM access
Cycle 9: No RAM access
Cycle 10: RAMRead { address: 0x7fffa000, value: 2 }
Cycle 11-59: Various RAM accesses...
Cycle 60: RAMWrite { address: 0x7fffb008, pre: 0, post: 1 }
Step-by-Step Construction
if let Some(dth_log) = dth_root_log {
let address_opt = remap_address(
cycle.ram_access().address() as u64,
&preprocessing.shared.memory_layout,
);
for j in 0..ram_d {
let index = address_opt.map(|address| {
((address as usize >> (dth_log * (ram_d - 1 - j))) % DTH_ROOT_OF_K) as u8
});
batch_ref.ram_ra[j][i] = index;
}
}
For each cycle i:
Step 1: Get Memory Address (if any)
let mem_address = cycle.ram_access().address();
If no RAM access, all RamRa[j][i] = None.
Step 2: Remap Address to Relative Offset
let address_opt = remap_address(mem_address, &memory_layout);
The remap_address function converts absolute
memory addresses into relative word
indices:
pub fn remap_address(address: u64, memory_layout: &MemoryLayout) -> Option<u64> {
if address == 0 {
return None; // Address 0 means no memory access
}
let lowest_address = memory_layout.get_lowest_address();
if address >= lowest_address {
Some((address - lowest_address) / 8) // Divide by 8 for word index
} else {
panic!("Unexpected address {address}")
}
}
- Returns
Nonefor address 0 - indicates no memory access - Subtracts lowest_address - creates relative offset
from program’s memory base
- In your program:
lowest_address = 0x7FFF8000 = 2147450880
- In your program:
- Divides by 8 - converts byte address to word index (since memory is stored as 8-byte words)
Why divide by 8? Remember RAMPreprocessing stores
memory as Vec<u64> (8-byte words). The remapped
address is actually a word index into that array, not a
byte offset!
Examples from your program:
// Byte address: 0x7FFFA000 (reading at location)
// lowest_address: 0x7FFF8000
// Remapped: (0x7FFFA000 - 0x7FFF8000) / 8 = 0x2000 / 8 = 8192 / 8 = 1024 (word index 1024)
// Byte address: 0x7FFFB000 (reading at location)
// lowest_address: 0x7FFF8000
// Remapped: (0x7FFFB000 - 0x7FFF8000) / 8 = 0x3000 / 8 = 12288 / 8 = 1536 (word index 1536)
// Byte address: 0x7FFFC008 (writing at location)
// lowest_address: 0x7FFF8000
// Remapped: (0x7FFFC008 - 0x7FFF8000) / 8 = 0x4008 / 8 = 16392 / 8 = 2049 (word index 2049)
Step 3: Chunk the Remapped Address
for j in 0..ram_d {
let shift = dth_log * (ram_d - 1 - j);
let index = (remapped_address >> shift) % DTH_ROOT_OF_K;
RamRa[j][i] = Some(index as u8);
}
Chunking parameters: - ram_d = number
of chunks (determined by memory size) - dth_log = bits per
chunk - DTH_ROOT_OF_K = 2^dth_log (size of each lookup
table)
Concrete Example 1: Cycle 10 (Memory Read)
Cycle 10: LD {
instruction: LD {
address: 2147483714, // Instruction at 0x80000042
operands: FormatLoad { rd: 34, rs1: 33, imm: 0 },
virtual_sequence_remaining: Some(5),
},
ram_access: RAMRead {
address: 2147459072, // 0x7fffa000 (input buffer)
value: 2,
}
}
Step 1: Get RAM address
mem_address = 2147459072 // 0x7fffa000
Step 2: Remap to relative word index
lowest_address = 0x7fff8000 // Start of memory region
byte_offset = 0x7fffa000 - 0x7fff8000 = 0x2000 = 8192 bytes
word_index = 8192 / 8 = 1024
Step 3: Chunk the word index
With ram_d = 2 and dth_log = 8 (so
DTH_ROOT_OF_K = 256):
// 1024 in binary: 0000 0100 0000 0000
// 1024 = 4 * 256 + 0
j = 0: shift = 8
index = (1024 >> 8) % 256 = 4
RamRa[0][10] = Some(4)
j = 1: shift = 0
index = (1024 >> 0) % 256 = 0
RamRa[1][10] = Some(0)
Result: RAM byte address 0x7fffa000 →
word index 1024 → chunked as [4, 0]
Verify reconstruction:
4 * 256 + 0 = 1024
This matches our actual RamRa output!
Concrete Example 2: Cycle 60 (Memory Write)
Cycle 60: SD {
ram_access: RAMWrite {
address: 2147467272, // 0x7FFFC008
pre_value: 0,
post_value: 1,
}
}
Step 1: Get RAM address
mem_address = 2147467272 // 0x7FFFC008
Step 2: Remap to relative word index
lowest_address = 0x7FFF8000 // Start of memory region
byte_offset = 0x7FFFC008 - 0x7FFF8000 = 0x4008 = 16392 bytes
word_index = 16392 / 8 = 2049
Step 3: Chunk the word index
With ram_d = 2 and dth_log = 8:
// 2049 in binary: 0000 1000 0000 0001
// 2049 = 8 * 256 + 1
j = 0: shift = 8
index = (2049 >> 8) % 256 = 8
RamRa[0][60] = Some(8)
j = 1: shift = 0
index = (2049 >> 0) % 256 = 1
RamRa[1][60] = Some(1)
Result: RAM byte address 0x7FFFC008 →
word index 2049 → chunked as [8, 1]
Verify reconstruction:
8 * 256 + 1 = 2048 + 1 = 2049 ✓
Concrete Example 3: Cycle 8 (No Memory Access)
Cycle 8: ADDI {
// No RAM access - just register operations
}
Result:
RamRa[0][8] = None
RamRa[1][8] = None
Not every cycle accesses memory, so we use
Option<u8> to represent “no access.”
The Complete RamRa Polynomials (Your Actual Trace)
After processing all 63 cycles in your trace, here are the actual values:
RamRa[0] = [
None, None, None, None, None, None, None, None, None, None,
Some(4), // Cycle 10: word index 1024 (input buffer read)
None, None, None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None, None, None,
None, None, None, None, None, None,
Some(6), // Cycle 37: word index 1536
None, None, None, None, None, None, None, None, None,
Some(6), // Cycle 47: word index 1536 (same address)
None, None,
Some(8), // Cycle 50: word index 2049 (output buffer)
None, None, None, None, None, None, None, None, None,
Some(8), // Cycle 60: word index 2049 (same address, write)
None, None, None,
]
RamRa[1] = [
None, None, None, None, None, None, None, None, None, None,
Some(0), // Cycle 10: chunk 1 of word 1024
None, None, None, None, None, None, None, None, None, None,
None, None, None, None, None, None, None, None, None, None,
None, None, None, None, None, None,
Some(0), // Cycle 37: chunk 1 of word 1536
None, None, None, None, None, None, None, None, None,
Some(0), // Cycle 47: chunk 1 of word 1536
None, None,
Some(1), // Cycle 50: chunk 1 of word 2049
None, None, None, None, None, None, None, None, None,
Some(1), // Cycle 60: chunk 1 of word 2049
None, None, None,
]
Memory access summary: - Cycle 10:
Read from word 1024 → [4, 0] - Cycle 37:
Access word 1536 → [6, 0] - Cycle 47:
Access word 1536 → [6, 0] (same location as cycle 37) -
Cycle 50: Read from word 2049 → [8, 1] -
Cycle 60: Write to word 2049 → [8, 1]
(same location as cycle 50)
Notice that cycles 37 and 47 access the same word (1536), and cycles 50 and 60 access the same word (2049).
Connection to RAMPreprocessing
Remember the RAMPreprocessing structure we built earlier? It stores memory as 8-byte words:
RAMPreprocessing {
min_bytecode_address: 0x7FFF8000, // = 2147450880 = lowest_address
bytecode_words: [
... // Word 0: bytes 0x7FFF8000-0x7FFF8007
... // Word 1024: bytes 0x7FFFA000-0x7FFFA007
... // Word 1536: bytes 0x7FFFB000-0x7FFFB007
... // Word 2049: bytes 0x7FFFC008-0x7FFFC00F
],
}
The remapping creates perfect alignment: -
RAMPreprocessing stores memory as
bytecode_words[word_index] - RamRa chunks
the same word_index for lookups - Both work with
word-aligned indices, not byte addresses
When the verifier checks memory accesses, they can: 1. Reconstruct
word index from RamRa chunks:
word_index = RamRa[0] * 256 + RamRa[1] 2. Look up in
RAMPreprocessing: bytecode_words[word_index] 3. Verify the
memory operation was correct
Why Option?
Unlike BytecodeRa where every cycle executes an instruction, not
every cycle accesses memory: - Cycles with
LD/SD/etc. → Some(chunk_value) -
Cycles with ADDI/MUL/etc. →
None
Summary
Input: - Memory accesses from trace (byte addresses)
- Memory layout (provides lowest_address for remapping) -
RAMPreprocessing (word-aligned storage)
Process: For each cycle i: 1. Check if cycle
accesses memory 2. If yes: Get absolute byte address 3. Remap to
relative word index: (address - lowest_address) / 8 4.
Chunk word index into ram_d pieces using bit shifts 5. Store chunks in
RamRa[0][i], RamRa[1][i], …,
RamRa[ram_d-1][i] 6. If no: Store None in all
RamRa polynomials
Output: ram_d polynomials tracking memory accesses as word indices, ready for memory consistency checks in the proof.