lines 1-3
Constants for the byte offsets we will read
1.equ ACCT0_KEY, 0x00102.equ ACCT0_DATA_LEN, 0x00583.equ ACCT0_DATA, 0x0060
Three constants, identical to the other sysvar-walking guards (fee_ceiling, compute_unit_floor). They name where account 0's pubkey, data length, and data block live inside the input region.
Why ACCT0_KEY = 0x0010, ACCT0_DATA_LEN = 0x0058, ACCT0_DATA = 0x0060? Same per-account header math as the other guards. 8 bytes for the u64 account count + 8 bytes for the dup tag/flags/padding = 16, where the 32-byte pubkey starts. Pubkey ends at 48, then 32 bytes of owner pubkey ends at 80, then 8-byte u64 lamports ends at 88, which is where the data_len u64 sits (0x58). Data starts at 96 (0x60). Account 0 here is the Instructions sysvar, so the data block is the serialized list of every instruction in the tx.
lines 7-20
Verify account 0 is the Instructions sysvar
7 lddw r2, sysvar_ix_key8 ldxdw r3, [r1 + ACCT0_KEY + 0]9 ldxdw r4, [r2 + 0]10 jne r3, r4, bad_account11 ldxdw r3, [r1 + ACCT0_KEY + 8]12 ldxdw r4, [r2 + 8]13 jne r3, r4, bad_account14 ldxdw r3, [r1 + ACCT0_KEY + 16]15 ldxdw r4, [r2 + 16]16 jne r3, r4, bad_account17 ldxdw r3, [r1 + ACCT0_KEY + 24]18 ldxdw r4, [r2 + 24]19 jne r3, r4, bad_account
Four 8-byte compares against the rodata sysvar pubkey constant (Sysvar1nstructions1111111111111111111111111). Any mismatch exits 3 with 'bad account'.
Why check at all? The SDK always wires the sysvar in, but a hand-crafted tx could pass any account. We refuse anything else before reading bytes we would otherwise interpret as serialized instructions. See the fee_ceiling walkthrough for the same pattern.
lines 22-31
Set up the data pointer, cache our own index and the total ix count
22 mov64 r3, r123 add64 r3, ACCT0_DATA24 25 mov64 r4, r326 add64 r4, r227 sub64 r4, 228 ldxh r9, [r4 + 0]29 30 ldxh r8, [r3 + 0]
r3 = pointer to the sysvar's serialized data (input region + 0x60). All subsequent ix lookups are relative to r3.
r9 = current_instruction_index, read from the LAST 2 bytes of the sysvar data. The runtime puts our own index there so a program can identify itself. We cache it because the outer loop will compare every ix index against r9 to skip the guard's own ix (more on that below).
r8 = num_instructions, the u16 at the very start of the sysvar data. r8 is the outer loop bound.
lines 33-45
Locate our own ix and read its data length
33 lsh64 r2, 134 add64 r2, r335 ldxh r4, [r2 + 2]36 add64 r4, r337 38 ldxh r2, [r4 + 0]39 mul64 r2, 3340 add64 r2, 3441 add64 r4, r242 43 ldxh r2, [r4 + 0]44 add64 r4, 2
Same sysvar walk as fee_ceiling: use the offsets table to find our own ix's serialized form, then skip past the per-ix header (num_accounts u16 + 33 bytes per account meta + 32-byte program_id) to land on the ix_data_len u16.
ldxh r2 reads num_accounts. mul64 r2, 33; add64 r2, 34 computes the offset to ix_data_len (skipping the num_accounts u16, the account metas, and the program_id). r4 += r2 leaves r4 pointing at ix_data_len.
ldxh r2 reads the length. add64 r4, 2 advances r4 to the first byte of ix_data itself.
lines 47-53
Read the allowlist count, validate the total length
47 jeq r6, 0, not_allowed48 49 mov64 r5, r650 lsh64 r5, 551 add64 r5, 152 jne r2, r5, bad_ix_data
ldxb r6, [r4 + 0] reads count (the first byte of our ix data). The layout is [u8 count][32 bytes * count]: one count byte, then count contiguous 32-byte pubkeys.
jeq r6, 0, not_allowed fast-fails on an empty allowlist. An empty list means 'nothing is permitted', every non-self ix would fail, so we exit 1 immediately instead of running the loop for nothing.
Expected length = 1 + 32 * count. r5 = r6 << 5 (left-shift by 5 is multiplication by 32, since 2^5 = 32), then add 1 for the count byte. jne r2, r5, bad_ix_data on mismatch (exit 2). Catches both truncation and trailing junk.
lines 55-61
Set up the start and end pointers for the allowlist
55 add64 r5, 156 57 mov64 r2, r658 lsh64 r2, 559 mov64 r6, r560 add64 r6, r2
r5 = r4 + 1 = pointer to the first 32-byte allowlist entry (skipping the count byte).
r6 = r5 + count * 32 = pointer to one byte past the last entry. We use r6 as a sentinel in the inner loop: when the scan pointer reaches r6, we have walked the whole allowlist without finding a match for the current ix.
lines 63-80
Outer loop: skip our own ix, look up the next program_id
63 64loop:65 jge r7, r8, ok66 67 jeq r7, r9, advance_outer68 69 mov64 r2, r770 lsh64 r2, 171 add64 r2, r372 ldxh r4, [r2 + 2]73 add64 r4, r374 75 ldxh r2, [r4 + 0]76 mul64 r2, 3377 add64 r2, 278 add64 r4, r279 mov64 r1, r4
jge r7, r8, ok exits the loop successfully when r7 reaches num_instructions (all top-level ixs checked, no violation).
jeq r7, r9, advance_outer skips the guard's own ix (where r7 equals current_instruction_index, which we cached in r9). This is the implicit-self-skip contract: the caller does not need to include this guard's program_id in the allowlist because the guard exempts itself. Without this branch, the user would have to list this program's own pubkey, which is awkward and circular.
For every other ix, locate its program_id using the offsets table (same pattern as fee_ceiling). Save the candidate program_id pointer in r1.
lines 84-102
Inner check: compare the candidate program_id against every allowlist entry
84 ldxdw r0, [r1 + 0]85 ldxdw r4, [r2 + 0]86 jne r0, r4, advance_inner87 ldxdw r0, [r1 + 8]88 ldxdw r4, [r2 + 8]89 jne r0, r4, advance_inner90 ldxdw r0, [r1 + 16]91 ldxdw r4, [r2 + 16]92 jne r0, r4, advance_inner93 ldxdw r0, [r1 + 24]94 ldxdw r4, [r2 + 24]95 jne r0, r4, advance_inner96 97 ja advance_outer98 99advance_inner:100 add64 r2, 32101 jge r2, r6, not_allowed102 ja check_inner
Inner loop body. Compare the candidate program_id (in memory at r1) against the current allowlist entry (in memory at r2), 8 bytes at a time. Four pairs of ldxdw + jne.
First mismatch jumps to advance_inner: bump r2 by 32 (move to the next allowlist entry), check if r2 reached r6 (the end sentinel we computed above). If yes, the candidate didn't match anything in the allowlist, fail with exit 1 (not_allowed). If no, loop back to check_inner with the new r2.
All four chunks match (this ix's program_id is in the allowlist), ja advance_outer to check the next ix.
lines 104-110
Outer advance and happy exit
104advance_outer:105 add64 r7, 1106 ja loop107 108ok:109 mov64 r0, 0110 exit
advance_outer: add64 r7, 1; ja loop. Increment the outer counter, restart.
ok: mov64 r0, 0; exit. All non-self ixs matched some entry in the allowlist. Success.
lines 112-131
Three failure paths, three exit codes
112not_allowed:113 lddw r1, msg_not_allowed114 mov64 r2, 11115 call sol_log_116 mov64 r0, 1117 exit118 119bad_ix_data:120 lddw r1, msg_bad121 mov64 r2, 11122 call sol_log_123 mov64 r0, 2124 exit125 126bad_account:127 lddw r1, msg_acct128 mov64 r2, 11129 call sol_log_130 mov64 r0, 3131 exit
not_allowed (exit 1): either the count was 0, or some ix targeted a program not in the allowlist. Log 'not allowed' (11 bytes).
bad_ix_data (exit 2): the declared count and the actual ix data length disagreed. Log 'bad ix data' (11 bytes).
bad_account (exit 3): account 0 was not the Instructions sysvar. Log 'bad account' (11 bytes).
lines 133-137
Read-only data
133.rodata134 sysvar_ix_key: .byte 0x06, 0xa7, 0xd5, 0x17, 0x18, 0x7b, 0xd1, 0x66, 0x35, 0xda, 0xd4, 0x04, 0x55, 0xfd, 0xc2, 0xc0, 0xc1, 0x24, 0xc6, 0x8f, 0x21, 0x56, 0x75, 0xa5, 0xdb, 0xba, 0xcb, 0x5f, 0x08, 0x00, 0x00, 0x00135 msg_not_allowed: .ascii "not allowed"136 msg_bad: .ascii "bad ix data"137 msg_acct: .ascii "bad account"
The Instructions sysvar pubkey constant (32 bytes) and three log strings. Total program size stays small because the loops are unrolled four wide (each pubkey compare is four ldxdw + four jne, no inner table machinery).