|
8 | 8 | * @see https://github.com/tomusdrw/JIPs/blob/td-jip6-ecalliloggin/JIP-6.md |
9 | 9 | */ |
10 | 10 |
|
| 11 | +import { z } from "zod"; |
| 12 | + |
11 | 13 | /** Register dump: mapping from register index to value */ |
12 | 14 | export type RegisterDump = Map<number, bigint>; |
13 | 15 |
|
@@ -121,6 +123,128 @@ export interface StateMismatch { |
121 | 123 | details?: string; |
122 | 124 | } |
123 | 125 |
|
| 126 | +// ============================================================================ |
| 127 | +// Zod Validation Schemas |
| 128 | +// ============================================================================ |
| 129 | + |
| 130 | +/** Zod schema for MemoryRead */ |
| 131 | +const MemoryReadSchema = z.object({ |
| 132 | + address: z.number(), |
| 133 | + length: z.number(), |
| 134 | + data: z.instanceof(Uint8Array), |
| 135 | +}); |
| 136 | + |
| 137 | +/** Zod schema for MemoryWrite */ |
| 138 | +const MemoryWriteSchema = z.object({ |
| 139 | + address: z.number(), |
| 140 | + length: z.number(), |
| 141 | + data: z.instanceof(Uint8Array), |
| 142 | +}); |
| 143 | + |
| 144 | +/** Zod schema for RegisterWrite */ |
| 145 | +const RegisterWriteSchema = z.object({ |
| 146 | + index: z.number(), |
| 147 | + value: z.bigint(), |
| 148 | +}); |
| 149 | + |
| 150 | +/** Zod schema for HostCallEntry */ |
| 151 | +const HostCallEntrySchema = z.object({ |
| 152 | + index: z.number(), |
| 153 | + pc: z.number(), |
| 154 | + gas: z.bigint(), |
| 155 | + registers: z.instanceof(Map), |
| 156 | + memoryReads: z.array(MemoryReadSchema), |
| 157 | + memoryWrites: z.array(MemoryWriteSchema), |
| 158 | + registerWrites: z.array(RegisterWriteSchema), |
| 159 | + gasAfter: z.bigint().nullable(), |
| 160 | + lineNumber: z.number(), |
| 161 | +}); |
| 162 | + |
| 163 | +/** Zod schema for StartEntry */ |
| 164 | +const StartEntrySchema = z.object({ |
| 165 | + pc: z.number(), |
| 166 | + gas: z.bigint(), |
| 167 | + registers: z.instanceof(Map), |
| 168 | +}); |
| 169 | + |
| 170 | +/** Zod schema for TracePrelude */ |
| 171 | +const TracePreludeSchema = z.object({ |
| 172 | + program: z.string().nullable(), |
| 173 | + start: StartEntrySchema.nullable(), |
| 174 | + initialMemoryWrites: z.array(MemoryWriteSchema), |
| 175 | +}); |
| 176 | + |
| 177 | +/** Zod schema for TerminationReason */ |
| 178 | +const TerminationReasonSchema = z.union([ |
| 179 | + z.object({ type: z.literal("HALT") }), |
| 180 | + z.object({ type: z.literal("PANIC"), argument: z.number() }), |
| 181 | + z.object({ type: z.literal("OOG") }), |
| 182 | +]); |
| 183 | + |
| 184 | +/** Zod schema for TerminationEntry */ |
| 185 | +const TerminationEntrySchema = z.object({ |
| 186 | + reason: TerminationReasonSchema, |
| 187 | + pc: z.number(), |
| 188 | + gas: z.bigint(), |
| 189 | + registers: z.instanceof(Map), |
| 190 | + lineNumber: z.number(), |
| 191 | +}); |
| 192 | + |
| 193 | +/** Zod schema for TraceParseError */ |
| 194 | +const TraceParseErrorSchema = z.object({ |
| 195 | + lineNumber: z.number(), |
| 196 | + line: z.string(), |
| 197 | + message: z.string(), |
| 198 | +}); |
| 199 | + |
| 200 | +/** Zod schema for ParsedTrace */ |
| 201 | +const ParsedTraceSchema = z.object({ |
| 202 | + contextLines: z.array(z.string()), |
| 203 | + prelude: TracePreludeSchema, |
| 204 | + hostCalls: z.array(HostCallEntrySchema), |
| 205 | + termination: TerminationEntrySchema.nullable(), |
| 206 | + errors: z.array(TraceParseErrorSchema), |
| 207 | +}); |
| 208 | + |
| 209 | +/** |
| 210 | + * Validate a parsed trace structure using Zod |
| 211 | + * @param trace The parsed trace to validate |
| 212 | + * @returns Zod safe parse result |
| 213 | + */ |
| 214 | +export function validateParsedTrace(trace: unknown) { |
| 215 | + return ParsedTraceSchema.safeParse(trace); |
| 216 | +} |
| 217 | + |
| 218 | +/** |
| 219 | + * Validate trace content string and return detailed validation result |
| 220 | + * @param content The trace content to validate |
| 221 | + * @returns Object containing validation result and any errors |
| 222 | + */ |
| 223 | +export function validateTraceContent(content: string): { |
| 224 | + success: boolean; |
| 225 | + errors: TraceParseError[]; |
| 226 | + parseErrors?: z.ZodError; |
| 227 | +} { |
| 228 | + const parsed = parseTrace(content); |
| 229 | + |
| 230 | + // Check for parse errors first |
| 231 | + if (parsed.errors.length > 0) { |
| 232 | + return { success: false, errors: parsed.errors }; |
| 233 | + } |
| 234 | + |
| 235 | + // Validate structure with Zod |
| 236 | + const validation = validateParsedTrace(parsed); |
| 237 | + if (!validation.success) { |
| 238 | + return { |
| 239 | + success: false, |
| 240 | + errors: parsed.errors, |
| 241 | + parseErrors: validation.error, |
| 242 | + }; |
| 243 | + } |
| 244 | + |
| 245 | + return { success: true, errors: [] }; |
| 246 | +} |
| 247 | + |
124 | 248 | // ============================================================================ |
125 | 249 | // Parsing utilities |
126 | 250 | // ============================================================================ |
@@ -646,17 +770,74 @@ export function findHostCallEntry( |
646 | 770 | hostCallIndex: number, |
647 | 771 | readMemory?: (address: number, length: number) => Uint8Array | null, |
648 | 772 | ): HostCallLookupResult { |
649 | | - // Try to find an exact match by PC |
650 | | - const matchingEntries = trace.hostCalls.slice(indexInTrace).filter((hc) => hc.pc === pc && hc.gas <= gas); |
| 773 | + // Get remaining entries from the current index |
| 774 | + const remainingEntries = trace.hostCalls.slice(indexInTrace); |
| 775 | + |
| 776 | + // Try to find an exact match by PC and host call index |
| 777 | + const matchingEntries = remainingEntries.filter((hc) => hc.pc === pc && hc.gas <= gas); |
651 | 778 |
|
652 | 779 | if (matchingEntries.length === 0) { |
653 | 780 | return { entry: null, mismatches: [] }; |
654 | 781 | } |
655 | 782 |
|
656 | | - // First try to find an entry whose hostCallIndex matches and has acceptable gas |
657 | | - const exactMatch = matchingEntries.find((hc) => hc.index === hostCallIndex && hc.gas <= gas); |
658 | | - // Use exact match if found, otherwise fall back to the first matching entry |
659 | | - const entry = exactMatch ?? matchingEntries[0]; |
| 783 | + // First try to find an entry whose hostCallIndex matches exactly |
| 784 | + // Prefer exact index match even if gas is slightly higher |
| 785 | + const exactIndexMatch = remainingEntries.find((hc) => hc.index === hostCallIndex && hc.pc === pc); |
| 786 | + |
| 787 | + // If we have an exact index match with acceptable gas, use it |
| 788 | + if (exactIndexMatch && exactIndexMatch.gas <= gas) { |
| 789 | + const mismatches: StateMismatch[] = []; |
| 790 | + |
| 791 | + // Check gas |
| 792 | + if (exactIndexMatch.gas !== gas) { |
| 793 | + mismatches.push({ |
| 794 | + field: "gas", |
| 795 | + expected: exactIndexMatch.gas.toString(), |
| 796 | + actual: gas.toString(), |
| 797 | + }); |
| 798 | + } |
| 799 | + |
| 800 | + // Check registers |
| 801 | + for (const [idx, expectedValue] of exactIndexMatch.registers) { |
| 802 | + const actualValue = registers[idx] ?? 0n; |
| 803 | + if (expectedValue !== actualValue) { |
| 804 | + mismatches.push({ |
| 805 | + field: "register", |
| 806 | + expected: `r${idx}=0x${expectedValue.toString(16)}`, |
| 807 | + actual: `r${idx}=0x${actualValue.toString(16)}`, |
| 808 | + details: `Register ${idx}`, |
| 809 | + }); |
| 810 | + } |
| 811 | + } |
| 812 | + |
| 813 | + // Check memory reads if readMemory function is provided |
| 814 | + if (readMemory) { |
| 815 | + for (const mr of exactIndexMatch.memoryReads) { |
| 816 | + const actualData = readMemory(mr.address, mr.length); |
| 817 | + if (actualData) { |
| 818 | + const expectedHex = Array.from(mr.data) |
| 819 | + .map((b) => b.toString(16).padStart(2, "0")) |
| 820 | + .join(""); |
| 821 | + const actualHex = Array.from(actualData) |
| 822 | + .map((b) => b.toString(16).padStart(2, "0")) |
| 823 | + .join(""); |
| 824 | + if (expectedHex !== actualHex) { |
| 825 | + mismatches.push({ |
| 826 | + field: "memread", |
| 827 | + expected: `0x${expectedHex}`, |
| 828 | + actual: `0x${actualHex}`, |
| 829 | + details: `Memory at 0x${mr.address.toString(16)} (${mr.length} bytes)`, |
| 830 | + }); |
| 831 | + } |
| 832 | + } |
| 833 | + } |
| 834 | + } |
| 835 | + |
| 836 | + return { entry: exactIndexMatch, mismatches }; |
| 837 | + } |
| 838 | + |
| 839 | + // Fall back to first matching entry by PC and gas |
| 840 | + const entry = matchingEntries[0]; |
660 | 841 | const mismatches: StateMismatch[] = []; |
661 | 842 |
|
662 | 843 | // Check PC |
|
0 commit comments