diff --git a/.agents/code-insights.md b/.agents/code-insights.md index e704a95860..3291812b39 100644 --- a/.agents/code-insights.md +++ b/.agents/code-insights.md @@ -4,3 +4,5 @@ - `DecodeModule` skips DWARF indexing when `dwarf.New` returns nil, preventing crashes on minimal modules. - DWARF lookups guard missing call-site metadata and tracing helpers skip absent debug info; tracing/DWARF/stylus coverage is still thin and needs focused tests. - Use `maintester.StripKnownDWARFWarnings` to drop the known DWARF warning before asserting stderr in examples and filecache integration tests. +- Stylus `emit_log` host hook now resolves ABI signatures and decodes topics/payloads locally (including arrays, dynamic bytes/strings), falling back to hash-only output for dynamic indexed params. +- Stylus log decoder now understands tuple parameter types (including nested/dynamic fields) and renders them as `(v0, v1, …)`; extend tests accordingly when adding new ABI shapes. diff --git a/.agents/others/emit_log_serde_spec.md b/.agents/others/emit_log_serde_spec.md new file mode 100644 index 0000000000..568ba8e4f6 --- /dev/null +++ b/.agents/others/emit_log_serde_spec.md @@ -0,0 +1,68 @@ +### emit_log Serialization Decoding Guide + +This document specifies how to decode the `Args` byte sequence that accompanies an `emit_log` hostio event in Stylus traces (as surfaced via `debug_traceCall`). + +#### Byte Sequence Structure + +``` +Offset Size Type (encoded) Meaning +0 4 bytes u32 (big-endian) Topic count `topicCount` +4 32 * n topic[n] (bytes32) n 32-byte topic hashes, contiguous +rest m bytes bytes (raw) Log data payload +``` + +`n` equals `topicCount`. `m` may be zero. No additional padding or length prefixes appear after the header. + +#### Decoding Procedure + +1. **Read Topic Count (`u32`)** + - Bytes `[0..4)` form a big-endian unsigned 32-bit integer. + - Conventionally limited to the range `0…4`; higher values indicate malformed input. + +2. **Extract Topics (`topicCount × bytes32`)** + - For each index `i` in `0…topicCount-1`, slice bytes `[4 + 32*i … 4 + 32*(i+1))`. + - Treat each slice as an EVM topic hash (32-byte word). + - Preserve the original order; `topic[0]` corresponds to `LOGn`’s `topic0`, etc. + +3. **Extract Log Data (`bytes`)** + - Remaining bytes starting at offset `4 + 32*topicCount` form the log payload. + - Interpret as arbitrary byte array (may be empty). + - No alignment or padding: payload length is `totalLen - 4 - 32*topicCount`. + +#### Mapping to EVM Event ABI + +- `topic[0]` (if `topicCount > 0`) is usually the Keccak-256 hash of the event signature (e.g., `keccak256("Transfer(address,address,uint256)")`). +- Additional topics represent indexed event parameters encoded as 32-byte ABI words. +- The data payload contains the concatenated ABI encoding of all non-indexed event parameters, identical to Ethereum’s event ABI rules. You must know the event signature to decode individual fields. + +#### ABI Decoding Cheat Sheet (Indexed Parameters → Topics) + +- **`uint` / `int` / `bool`**: 32-byte word; decode exactly as you would pop from the stack (big-endian, two’s complement for signed). +- **`address`**: rightmost 20 bytes of the 32-byte topic; leftmost 12 bytes are zero padding. +- **`bytes32` / `hash`**: topic already is the 32-byte value. +- **Static tuple or fixed-size array**: not supported directly; the entire tuple serializes to its Keccak-256 hash before being placed in a topic. +- **Dynamic types (string, bytes, dynamic arrays, dynamic tuples, structs)**: Solidity ABI stores `keccak256(value)` in the topic. You must use the original event parameters or compare hashes because the raw value is not present. + +#### ABI Decoding Cheat Sheet (Non-Indexed Parameters → Data Payload) + +Static types occupy fixed 32-byte slots; dynamic types use 32-byte offsets into a tail section. The following summary matches the Ethereum ABI specification (`soliditylang.org/docs/abi-spec.html`): + +- **`uint` / `int`**: big-endian two’s-complement in 32 bytes; `` must be ≤256 and divisible by 8. Example: decode by reading the 32-byte word as unsigned/signed integer. +- **`bool`**: same as `uint8`; value is `0` or `1`. +- **`address`**: rightmost 20 bytes contain the address; leftmost 12 bytes are zero padding. +- **`bytes32` / `keccak256` hashes**: 32 raw bytes. +- **Static `tuple` / fixed-size array (`T[k]`)**: concatenation of each element’s 32-byte encoding. +- **`bytes` / `string` / dynamic array**: + - Word `w0`: 32-byte offset (from start of the data section) to the actual payload. + - At `offset`: 32-byte length `L`. + - Followed by `ceil(L/32)` words of data (right-padded with zeros). +- **Dynamic tuple**: treat each component like a standalone field; static components appear inline, dynamic components hold offsets into the shared tail. + +Decoding recipe for payload: +1. Split the payload into 32-byte words. +2. For each parameter (in declaration order) apply the ABI rules: + - Static parameter: interpret its word(s) directly. + - Dynamic parameter: read its offset word, jump to `payloadStart + offset`, read length and the subsequent bytes. +3. Apply type-specific conversions (e.g., trim leading zeros for addresses and shorter ints, decode UTF-8 for strings, iterate arrays). + +Remember: Stylus does not prepend additional metadata—ABI semantics are identical to standard Ethereum logs. Use the event signature or ABI supplied by the contract to decide which decoding path to follow. diff --git a/AGENTS.md b/AGENTS.md index 227fbecca2..17b855d25f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,16 +5,16 @@ The file `.agents/code-insights.md` is your checklist before interacting the codebase. Treat it as mandatory reading and maintenance. - **Before starting any task, ALWAYS open and review** `.agents/code-insights.md` so you inherit the latest context. -- Capture new insights, architectural notes, domain knowledge, debugging breadcrumbs, surprising findings, edge cases, and other non-trivial behaviors in `.agents/code-insights.md`. -- Keep entries in that insights file clear, concise, and dated when helpful so others can trust the context quickly. -- Update the insights file continuously: add new learnings immediately, revise stale items, and REMOVE information the moment it stops being true. -- If you are unsure whether something belongs in `.agents/code-insights.md`, err on the side of writing it down. +- Capture only non-trivial insights—complex behaviors, surprising findings, architectural pivots, or tricky edge cases—in `.agents/code-insights.md`. +- Do not record routine task updates or obvious observations; keep the file focused on durable, high-signal knowledge. +- Keep entries concise so others can trust the context quickly, and prune or revise them when they stop being accurate or relevant. Maintaining `.agents/code-insights.md` is part of completing every task. ## Testing Standards - Always design and commit comprehensive tests that cover baseline behavior and all edge cases **before** implementing the associated functionality. +- Keep test output minimal: successful runs must be silent, and failures should surface only the information necessary to pinpoint the fault. - Consider a task complete only after all relevant tests run and pass. ## Code Comments diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go index 08a7d9a468..981ec03273 100644 --- a/cmd/wazero/wazero.go +++ b/cmd/wazero/wazero.go @@ -214,6 +214,10 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { flags.StringVar(&stylusTracePath, "stylus", "", "Imports the EVM hook functions and mocks their IO according the result of debug_traceTransaction in the path provided.") + var stylusSignatureMapPath string + flags.StringVar(&stylusSignatureMapPath, "stylus-signature-map", "", + "Signature map of the EVM events. Used to decode events.") + var traceDir string flags.StringVar(&traceDir, "trace-dir", "", "Directory where to save the trace record. If empty - no trace is produced. Default \"\".") @@ -344,7 +348,7 @@ func doRun(args []string, stdOut io.Writer, stdErr logging.Writer) int { var stylusState *stylus.StylusTrace if stylusTracePath != "" { - stylusState, err = stylus.Instantiate(ctx, rt, stylusTracePath, traceRecordPtr) + stylusState, err = stylus.Instantiate(ctx, rt, stylusTracePath, stylusSignatureMapPath, traceRecordPtr) if err != nil { fmt.Fprintf(stdErr, "error reading stylus trace: %v\n", err) return 1 diff --git a/internal/stylus/emit_log_decoder.go b/internal/stylus/emit_log_decoder.go new file mode 100644 index 0000000000..fa97c9e8b5 --- /dev/null +++ b/internal/stylus/emit_log_decoder.go @@ -0,0 +1,811 @@ +package stylus + +import ( + "encoding/hex" + "fmt" + "math" + "math/big" + "strconv" + "strings" + "unicode/utf8" +) + +type abiKind int + +const ( + abiInvalid abiKind = iota + abiUint + abiInt + abiBool + abiAddress + abiFixedBytes + abiBytes + abiString + abiArray + abiTuple +) + +type abiType struct { + kind abiKind + bitSize int + byteSize int + length int // -1 for dynamic arrays + elem *abiType + tuple []abiType + raw string +} + +func (t abiType) isDynamic() bool { + switch t.kind { + case abiBytes, abiString: + return true + case abiArray: + if t.length < 0 { + return true + } + if t.elem == nil { + return true + } + return t.elem.isDynamic() + case abiTuple: + for _, comp := range t.tuple { + if comp.isDynamic() { + return true + } + } + return false + default: + return false + } +} + +func (t abiType) staticSize() (int, error) { + switch t.kind { + case abiUint, abiInt, abiBool, abiAddress, abiFixedBytes: + return 32, nil + case abiArray: + if t.length < 0 { + return 0, fmt.Errorf("dynamic array has no static size") + } + if t.elem == nil { + return 0, fmt.Errorf("array missing element type") + } + elemSize, err := t.elem.staticSize() + if err != nil { + return 0, err + } + return t.length * elemSize, nil + case abiTuple: + if t.isDynamic() { + return 0, fmt.Errorf("tuple contains dynamic components") + } + total := 0 + for _, comp := range t.tuple { + size, err := comp.staticSize() + if err != nil { + return 0, err + } + total += size + } + return total, nil + default: + return 0, fmt.Errorf("type %q has no static size", t.raw) + } +} + +func (t abiType) topicEncodable() bool { + if t.isDynamic() { + return false + } + size, err := t.staticSize() + if err != nil { + return false + } + return size == 32 +} + +func parseTupleType(base, raw string) (abiType, error) { + var inner string + switch { + case strings.HasPrefix(base, "(") && strings.HasSuffix(base, ")"): + inner = base[1 : len(base)-1] + case strings.HasPrefix(base, "tuple(") && strings.HasSuffix(base, ")"): + inner = base[len("tuple(") : len(base)-1] + default: + return abiType{kind: abiInvalid, raw: raw}, fmt.Errorf("unrecognized tuple type %q", raw) + } + + components := []abiType{} + if strings.TrimSpace(inner) != "" { + parts, err := splitTupleComponents(inner) + if err != nil { + return abiType{kind: abiInvalid, raw: raw}, err + } + components = make([]abiType, len(parts)) + for i, part := range parts { + child, err := parseABIType(part) + if err != nil { + return abiType{kind: abiInvalid, raw: raw}, err + } + components[i] = child + } + } + + return abiType{kind: abiTuple, tuple: components, raw: raw}, nil +} + +func parseABIType(typeStr string) (abiType, error) { + clean := strings.TrimSpace(typeStr) + clean = strings.ReplaceAll(clean, "payable", "") + clean = strings.ReplaceAll(clean, " ", "") + if clean == "" { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("empty type") + } + + baseBuilder := strings.Builder{} + arrayDims := []int{} + depth := 0 + for i := 0; i < len(clean); { + ch := clean[i] + switch ch { + case '(': + depth++ + baseBuilder.WriteByte(ch) + i++ + case ')': + if depth == 0 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unbalanced parentheses in type %q", typeStr) + } + depth-- + baseBuilder.WriteByte(ch) + i++ + case '[': + if depth > 0 { + baseBuilder.WriteByte(ch) + i++ + continue + } + end := strings.IndexByte(clean[i:], ']') + if end == -1 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unclosed array dimension in %q", typeStr) + } + end += i + dimStr := clean[i+1 : end] + if dimStr == "" { + arrayDims = append(arrayDims, -1) + } else { + dim, err := strconv.ParseInt(dimStr, 10, 32) + if err != nil { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("invalid array length %q in type %q", dimStr, typeStr) + } + if dim < 0 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("negative array length in type %q", typeStr) + } + arrayDims = append(arrayDims, int(dim)) + } + i = end + 1 + default: + baseBuilder.WriteByte(ch) + i++ + } + } + + if depth != 0 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unbalanced parentheses in type %q", typeStr) + } + + base := baseBuilder.String() + t := abiType{raw: typeStr} + + switch { + case strings.HasPrefix(base, "(") && strings.HasSuffix(base, ")"), + strings.HasPrefix(base, "tuple(") && strings.HasSuffix(base, ")"): + tuple, err := parseTupleType(base, typeStr) + if err != nil { + return abiType{kind: abiInvalid, raw: typeStr}, err + } + t = tuple + case base == "address": + t.kind = abiAddress + case base == "bool": + t.kind = abiBool + case base == "string": + t.kind = abiString + case base == "bytes": + t.kind = abiBytes + case strings.HasPrefix(base, "uint"): + size := 256 + if len(base) > len("uint") { + val, err := strconv.Atoi(base[len("uint"):]) + if err != nil { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("invalid uint size in type %q", typeStr) + } + size = val + } + if size <= 0 || size > 256 || size%8 != 0 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unsupported uint size %d in type %q", size, typeStr) + } + t.kind = abiUint + t.bitSize = size + case strings.HasPrefix(base, "int"): + size := 256 + if len(base) > len("int") { + val, err := strconv.Atoi(base[len("int"):]) + if err != nil { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("invalid int size in type %q", typeStr) + } + size = val + } + if size <= 0 || size > 256 || size%8 != 0 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unsupported int size %d in type %q", size, typeStr) + } + t.kind = abiInt + t.bitSize = size + case strings.HasPrefix(base, "bytes"): + if len(base) == len("bytes") { + t.kind = abiBytes + } else { + v, err := strconv.Atoi(base[len("bytes"):]) + if err != nil { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("invalid fixed bytes size in type %q", typeStr) + } + if v <= 0 || v > 32 { + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unsupported fixed bytes size %d in type %q", v, typeStr) + } + t.kind = abiFixedBytes + t.byteSize = v + } + default: + return abiType{kind: abiInvalid, raw: typeStr}, fmt.Errorf("unsupported type %q", typeStr) + } + + if len(arrayDims) == 0 { + return t, nil + } + + current := t + for i := len(arrayDims) - 1; i >= 0; i-- { + length := arrayDims[i] + elemCopy := current + current = abiType{ + kind: abiArray, + length: length, + elem: &elemCopy, + raw: typeStr, + } + } + return current, nil +} + +type parameterValue struct { + param eventParameter + value string + warning string +} + +func decodeEventParameters(sig eventSignature, topics [][]byte, data []byte) ([]parameterValue, []string) { + values := make([]parameterValue, len(sig.Params)) + for i, param := range sig.Params { + values[i] = parameterValue{param: param} + } + + var warnings []string + + targets := []dataTarget{} + topicIdx := 0 + + for i, param := range sig.Params { + typ, err := parseABIType(param.Type) + if err != nil { + values[i].value = fmt.Sprintf("", err.Error()) + continue + } + + if param.Indexed { + if topicIdx >= len(topics) { + label := parameterLabel(param, i) + values[i].value = "" + warnings = append(warnings, fmt.Sprintf("insufficient topics to decode indexed parameter %s", label)) + continue + } + + topic := topics[topicIdx] + topicIdx++ + + if typ.isDynamic() || !typ.topicEncodable() { + values[i].value = fmt.Sprintf("keccak256=%s", hexBytes(topic)) + if typ.isDynamic() { + values[i].warning = "dynamic indexed parameters expose only the keccak256 hash" + } else { + values[i].warning = "indexed value exceeds 32 bytes; showing raw topic hash" + } + continue + } + + decoded, err := decodeStaticTopicValue(typ, topic) + if err != nil { + values[i].value = hexBytes(topic) + values[i].warning = fmt.Sprintf("decode error: %v", err) + continue + } + + values[i].value = decoded + } else { + targets = append(targets, dataTarget{paramIdx: i, typ: typ}) + } + } + + if topicIdx < len(topics) { + warnings = append(warnings, fmt.Sprintf("unused topics: %d", len(topics)-topicIdx)) + } + + dataResults, dataWarnings := decodeNonIndexedData(targets, data) + for i, result := range dataResults { + target := targets[i] + if result.value != "" { + values[target.paramIdx].value = result.value + } + if result.warning != "" { + values[target.paramIdx].warning = appendWarning(values[target.paramIdx].warning, result.warning) + } + } + + warnings = append(warnings, dataWarnings...) + + return values, warnings +} + +type dataTarget struct { + paramIdx int + typ abiType +} + +type decodeResult struct { + value string + warning string +} + +func decodeNonIndexedData(targets []dataTarget, data []byte) ([]decodeResult, []string) { + results := make([]decodeResult, len(targets)) + warnings := []string{} + + type dynamicJob struct { + targetIdx int + typ abiType + offset int + } + + dynamicJobs := []dynamicJob{} + cursor := 0 + + for i, target := range targets { + if target.typ.isDynamic() { + if cursor+32 > len(data) { + results[i].value = "" + results[i].warning = "not enough bytes for dynamic value offset" + warnings = append(warnings, fmt.Sprintf("parameter %d offset out of bounds", target.paramIdx)) + cursor = len(data) + continue + } + offset, err := wordToInt(data[cursor : cursor+32]) + if err != nil { + results[i].value = "" + results[i].warning = err.Error() + warnings = append(warnings, fmt.Sprintf("parameter %d has invalid offset", target.paramIdx)) + cursor += 32 + continue + } + dynamicJobs = append(dynamicJobs, dynamicJob{targetIdx: i, typ: target.typ, offset: offset}) + cursor += 32 + continue + } + + size, err := target.typ.staticSize() + if err != nil { + results[i].value = "" + results[i].warning = err.Error() + continue + } + + if cursor+size > len(data) { + results[i].value = "" + results[i].warning = "not enough bytes for static value" + warnings = append(warnings, fmt.Sprintf("parameter %d static data truncated", target.paramIdx)) + cursor = len(data) + continue + } + + chunk := data[cursor : cursor+size] + value, err := decodeStaticBytes(target.typ, chunk) + if err != nil { + results[i].value = hexBytes(chunk) + results[i].warning = err.Error() + } else { + results[i].value = value + } + + cursor += size + } + + for _, job := range dynamicJobs { + if job.offset < 0 || job.offset > len(data) { + results[job.targetIdx].value = "" + results[job.targetIdx].warning = "offset outside payload" + warnings = append(warnings, fmt.Sprintf("parameter %d offset outside payload", targets[job.targetIdx].paramIdx)) + continue + } + + value, err := decodeDynamicValue(job.typ, data, job.offset) + if err != nil { + results[job.targetIdx].value = fmt.Sprintf("", err) + results[job.targetIdx].warning = err.Error() + } else { + results[job.targetIdx].value = value + } + } + + return results, warnings +} + +func decodeStaticBytes(t abiType, chunk []byte) (string, error) { + switch t.kind { + case abiUint: + return decodeUint(chunk), nil + case abiInt: + return decodeInt(chunk), nil + case abiBool: + if len(chunk) != 32 { + return "", fmt.Errorf("invalid bool size %d", len(chunk)) + } + val := chunk[len(chunk)-1] + if val == 0 { + return "false", nil + } + if val == 1 { + return "true", nil + } + return "", fmt.Errorf("invalid bool value 0x%x", chunk) + case abiAddress: + if len(chunk) != 32 { + return "", fmt.Errorf("invalid address size %d", len(chunk)) + } + return fmt.Sprintf("0x%s", hex.EncodeToString(chunk[12:])), nil + case abiFixedBytes: + if len(chunk) != 32 { + return "", fmt.Errorf("invalid fixed bytes size %d", len(chunk)) + } + return fmt.Sprintf("0x%s", hex.EncodeToString(chunk[:t.byteSize])), nil + case abiTuple: + if t.isDynamic() { + return "", fmt.Errorf("dynamic tuple treated as static") + } + return decodeStaticTuple(t, chunk) + case abiArray: + if t.length < 0 { + return "", fmt.Errorf("dynamic array treated as static") + } + if t.elem == nil { + return "", fmt.Errorf("array missing element type") + } + elemSize, err := t.elem.staticSize() + if err != nil { + return "", err + } + expected := elemSize * t.length + if len(chunk) != expected { + return "", fmt.Errorf("array chunk size %d does not match expected %d", len(chunk), expected) + } + items := make([]string, t.length) + for i := 0; i < t.length; i++ { + start := i * elemSize + value, err := decodeStaticBytes(*t.elem, chunk[start:start+elemSize]) + if err != nil { + return "", err + } + items[i] = value + } + return "[" + strings.Join(items, ", ") + "]", nil + default: + return "", fmt.Errorf("unsupported static type %q", t.raw) + } +} + +func decodeDynamicValue(t abiType, data []byte, offset int) (string, error) { + switch t.kind { + case abiBytes: + return decodeDynamicBytes(data, offset) + case abiString: + return decodeDynamicString(data, offset) + case abiArray: + if t.elem == nil { + return "", fmt.Errorf("array missing element type") + } + return decodeDynamicArray(t, data, offset) + case abiTuple: + return decodeDynamicTuple(t, data, offset) + default: + return "", fmt.Errorf("type %q is not dynamic", t.raw) + } +} + +func decodeDynamicBytes(data []byte, offset int) (string, error) { + if offset+32 > len(data) { + return "", fmt.Errorf("dynamic bytes length out of bounds") + } + + length, err := wordToInt(data[offset : offset+32]) + if err != nil { + return "", err + } + + start := offset + 32 + if length == 0 { + return "0x", nil + } + + if start+length > len(data) { + return "", fmt.Errorf("dynamic bytes truncated: need %d bytes", length) + } + + return fmt.Sprintf("0x%s", hex.EncodeToString(data[start:start+length])), nil +} + +func decodeDynamicString(data []byte, offset int) (string, error) { + if offset+32 > len(data) { + return "", fmt.Errorf("string length out of bounds") + } + + length, err := wordToInt(data[offset : offset+32]) + if err != nil { + return "", err + } + + start := offset + 32 + if start+length > len(data) { + return "", fmt.Errorf("string bytes truncated") + } + + value := data[start : start+length] + if utf8.Valid(value) { + return strconv.Quote(string(value)), nil + } + return fmt.Sprintf("0x%s", hex.EncodeToString(value)), nil +} + +func decodeDynamicArray(t abiType, data []byte, offset int) (string, error) { + if offset+32 > len(data) { + return "", fmt.Errorf("array length out of bounds") + } + + length, err := wordToInt(data[offset : offset+32]) + if err != nil { + return "", err + } + + start := offset + 32 + if length == 0 { + return "[]", nil + } + + values := make([]string, length) + if t.elem.isDynamic() { + headSize := 32 * length + if start+headSize > len(data) { + return "", fmt.Errorf("array head truncated") + } + + offsets := make([]int, length) + for i := 0; i < length; i++ { + word := data[start+i*32 : start+(i+1)*32] + ptr, err := wordToInt(word) + if err != nil { + return "", fmt.Errorf("invalid offset for element %d: %w", i, err) + } + offsets[i] = ptr + } + + for i := 0; i < length; i++ { + elemOffset := start + offsets[i] + if elemOffset < start || elemOffset > len(data) { + return "", fmt.Errorf("element %d offset outside payload", i) + } + value, err := decodeDynamicValue(*t.elem, data, elemOffset) + if err != nil { + return "", err + } + values[i] = value + } + } else { + elemSize, err := t.elem.staticSize() + if err != nil { + return "", err + } + total := elemSize * length + if start+total > len(data) { + return "", fmt.Errorf("array data truncated") + } + for i := 0; i < length; i++ { + elemStart := start + i*elemSize + val, err := decodeStaticBytes(*t.elem, data[elemStart:elemStart+elemSize]) + if err != nil { + return "", err + } + values[i] = val + } + } + + return "[" + strings.Join(values, ", ") + "]", nil +} + +func decodeStaticTopicValue(t abiType, topic []byte) (string, error) { + if len(topic) != 32 { + return "", fmt.Errorf("topic length %d", len(topic)) + } + return decodeStaticBytes(t, topic) +} + +func splitTupleComponents(spec string) ([]string, error) { + spec = strings.TrimSpace(spec) + if spec == "" { + return nil, nil + } + + var result []string + depth := 0 + start := 0 + for i := 0; i < len(spec); i++ { + switch spec[i] { + case '(': + depth++ + case ')': + if depth == 0 { + return nil, fmt.Errorf("unbalanced parentheses in tuple type (%s)", spec) + } + depth-- + case ',': + if depth == 0 { + part := strings.TrimSpace(spec[start:i]) + if part == "" { + return nil, fmt.Errorf("empty component in tuple type (%s)", spec) + } + result = append(result, part) + start = i + 1 + } + } + } + + if depth != 0 { + return nil, fmt.Errorf("unbalanced parentheses in tuple type (%s)", spec) + } + + tail := strings.TrimSpace(spec[start:]) + if tail == "" { + return nil, fmt.Errorf("empty component in tuple type (%s)", spec) + } + result = append(result, tail) + return result, nil +} + +func decodeStaticTuple(t abiType, chunk []byte) (string, error) { + values := make([]string, len(t.tuple)) + cursor := 0 + for i, comp := range t.tuple { + size, err := comp.staticSize() + if err != nil { + return "", err + } + if cursor+size > len(chunk) { + return "", fmt.Errorf("tuple component %d truncated", i) + } + val, err := decodeStaticBytes(comp, chunk[cursor:cursor+size]) + if err != nil { + return "", err + } + values[i] = val + cursor += size + } + return "(" + strings.Join(values, ", ") + ")", nil +} + +func decodeDynamicTuple(t abiType, data []byte, offset int) (string, error) { + values := make([]string, len(t.tuple)) + type tupleJob struct { + index int + typ abiType + offset int + } + var jobs []tupleJob + + cursor := offset + for i, comp := range t.tuple { + if comp.isDynamic() { + if cursor+32 > len(data) { + return "", fmt.Errorf("tuple component %d offset out of bounds", i) + } + ptr, err := wordToInt(data[cursor : cursor+32]) + if err != nil { + return "", fmt.Errorf("invalid tuple component %d offset: %w", i, err) + } + jobs = append(jobs, tupleJob{index: i, typ: comp, offset: ptr}) + cursor += 32 + continue + } + size, err := comp.staticSize() + if err != nil { + return "", err + } + if cursor+size > len(data) { + return "", fmt.Errorf("tuple component %d truncated", i) + } + val, err := decodeStaticBytes(comp, data[cursor:cursor+size]) + if err != nil { + return "", err + } + values[i] = val + cursor += size + } + + for _, job := range jobs { + elemOffset := offset + job.offset + if elemOffset < offset || elemOffset > len(data) { + return "", fmt.Errorf("tuple component %d offset outside payload", job.index) + } + val, err := decodeDynamicValue(job.typ, data, elemOffset) + if err != nil { + return "", err + } + values[job.index] = val + } + + return "(" + strings.Join(values, ", ") + ")", nil +} + +func decodeUint(word []byte) string { + val := new(big.Int).SetBytes(word) + return val.String() +} + +func decodeInt(word []byte) string { + val := new(big.Int).SetBytes(word) + if len(word) > 0 && word[0]&0x80 != 0 { + max := new(big.Int).Lsh(big.NewInt(1), uint(len(word))*8) + val.Sub(val, max) + } + return val.String() +} + +func wordToInt(word []byte) (int, error) { + if len(word) != 32 { + return 0, fmt.Errorf("word length %d", len(word)) + } + val := new(big.Int).SetBytes(word) + if val.Sign() < 0 { + return 0, fmt.Errorf("negative offset") + } + if val.BitLen() > 31 { + if !val.IsInt64() { + return 0, fmt.Errorf("value exceeds signed 64-bit range") + } + } + if val.Cmp(big.NewInt(math.MaxInt)) > 0 { + return 0, fmt.Errorf("value %s exceeds limits", val.String()) + } + return int(val.Int64()), nil +} + +func parameterLabel(param eventParameter, idx int) string { + if param.Name != "" { + return param.Name + } + return fmt.Sprintf("arg%d", idx) +} + +func appendWarning(existing, additional string) string { + if existing == "" { + return additional + } + if additional == "" { + return existing + } + return existing + "; " + additional +} diff --git a/internal/stylus/emit_log_decoder_test.go b/internal/stylus/emit_log_decoder_test.go new file mode 100644 index 0000000000..bbae69b0dd --- /dev/null +++ b/internal/stylus/emit_log_decoder_test.go @@ -0,0 +1,196 @@ +package stylus + +import ( + "encoding/hex" + "math/big" + "testing" +) + +func TestDecodeEventParameters_StaticTypes(t *testing.T) { + fromAddr := bytesRepeat([]byte{0xde, 0xad}, 10) + toAddr := bytesRepeat([]byte{0xca, 0xfe}, 10) + + sig := eventSignature{ + Name: "Transfer", + Params: []eventParameter{ + {Type: "address", Name: "from", Indexed: true}, + {Type: "address", Name: "to", Indexed: true}, + {Type: "uint256", Name: "value"}, + }, + } + + topics := [][]byte{ + padAddress(fromAddr), + padAddress(toAddr), + } + + data := make([]byte, 32) + big.NewInt(12345).FillBytes(data) + + values, warnings := decodeEventParameters(sig, topics, data) + if len(warnings) != 0 { + t.Fatalf("unexpected warnings: %v", warnings) + } + + if got, want := values[0].value, "0x"+hex.EncodeToString(fromAddr); got != want { + t.Fatalf("from value = %s, want %s", got, want) + } + if got, want := values[1].value, "0x"+hex.EncodeToString(toAddr); got != want { + t.Fatalf("to value = %s, want %s", got, want) + } + if got, want := values[2].value, "12345"; got != want { + t.Fatalf("amount value = %s, want %s", got, want) + } +} + +func TestDecodeEventParameters_DynamicBytes(t *testing.T) { + sig := eventSignature{ + Name: "Data", + Params: []eventParameter{ + {Type: "bytes", Name: "blob"}, + }, + } + + value := []byte{0x01, 0x02, 0x03} + data := make([]byte, 96) + copy(data[0:32], word(big.NewInt(32))) + copy(data[32:64], word(big.NewInt(int64(len(value))))) + copy(data[64:], padRight(value, 32)) + + values, warnings := decodeEventParameters(sig, nil, data) + if len(warnings) != 0 { + t.Fatalf("unexpected warnings: %v", warnings) + } + if got, want := values[0].value, "0x010203"; got != want { + t.Fatalf("blob value = %s, want %s", got, want) + } +} + +func TestDecodeEventParameters_DynamicArray(t *testing.T) { + sig := eventSignature{ + Name: "Values", + Params: []eventParameter{ + {Type: "uint256[]", Name: "values"}, + }, + } + + data := make([]byte, 32*5) + copy(data[0:32], word(big.NewInt(32))) + copy(data[32:64], word(big.NewInt(2))) + copy(data[64:96], word(big.NewInt(1))) + copy(data[96:128], word(big.NewInt(2))) + + values, warnings := decodeEventParameters(sig, nil, data) + if len(warnings) != 0 { + t.Fatalf("unexpected warnings: %v", warnings) + } + if got, want := values[0].value, "[1, 2]"; got != want { + t.Fatalf("values = %s, want %s", got, want) + } +} + +func TestDecodeEventParameters_IndexedDynamic(t *testing.T) { + sig := eventSignature{ + Name: "Message", + Params: []eventParameter{ + {Type: "bytes", Name: "payload", Indexed: true}, + {Type: "uint256", Name: "id"}, + }, + } + + topic := word(big.NewInt(0x1234)) + data := word(big.NewInt(99)) + + values, warnings := decodeEventParameters(sig, [][]byte{topic}, data) + if len(warnings) != 0 { + t.Fatalf("unexpected warnings: %v", warnings) + } + + if got, want := values[0].value, "keccak256=0x"+hex.EncodeToString(topic); got != want { + t.Fatalf("payload value = %s, want %s", got, want) + } + if values[0].warning == "" { + t.Fatalf("expected warning for dynamic indexed parameter") + } + if got, want := values[1].value, "99"; got != want { + t.Fatalf("id value = %s, want %s", got, want) + } +} + +func TestDecodeEventParameters_TupleStatic(t *testing.T) { + sig := eventSignature{ + Name: "Pair", + Params: []eventParameter{ + {Type: "(uint256,bool)", Name: "pair"}, + }, + } + + data := make([]byte, 64) + copy(data[0:32], word(big.NewInt(42))) + boolWord := make([]byte, 32) + boolWord[31] = 1 + copy(data[32:64], boolWord) + + values, warnings := decodeEventParameters(sig, nil, data) + if len(warnings) != 0 { + t.Fatalf("unexpected warnings: %v", warnings) + } + if got, want := values[0].value, "(42, true)"; got != want { + t.Fatalf("pair value = %s, want %s", got, want) + } +} + +func TestDecodeEventParameters_TupleWithDynamic(t *testing.T) { + sig := eventSignature{ + Name: "Details", + Params: []eventParameter{ + {Type: "(address,string)", Name: "details"}, + }, + } + + addr := bytesRepeat([]byte{0x11}, 20) + message := []byte("hello") + + data := make([]byte, 32*5) + copy(data[0:32], word(big.NewInt(32))) + copy(data[32:64], padAddress(addr)) + copy(data[64:96], word(big.NewInt(64))) + copy(data[96:128], word(big.NewInt(int64(len(message))))) + copy(data[128:], padRight(message, 32)) + + values, warnings := decodeEventParameters(sig, nil, data) + if len(warnings) != 0 { + t.Fatalf("unexpected warnings: %v", warnings) + } + + wantAddr := "0x" + hex.EncodeToString(addr) + if got, want := values[0].value, "("+wantAddr+", \"hello\")"; got != want { + t.Fatalf("details value = %s, want %s", got, want) + } +} + +func padAddress(addr []byte) []byte { + word := make([]byte, 32) + copy(word[32-len(addr):], addr) + return word +} + +func word(i *big.Int) []byte { + out := make([]byte, 32) + i.FillBytes(out) + return out +} + +func bytesRepeat(pattern []byte, count int) []byte { + out := make([]byte, len(pattern)*count) + for i := 0; i < count; i++ { + copy(out[i*len(pattern):], pattern) + } + return out +} + +func padRight(src []byte, size int) []byte { + out := make([]byte, size) + copy(out, src) + return out +} diff --git a/internal/stylus/event_signature.go b/internal/stylus/event_signature.go new file mode 100644 index 0000000000..5b3b319858 --- /dev/null +++ b/internal/stylus/event_signature.go @@ -0,0 +1,147 @@ +package stylus + +import ( + "fmt" + "strings" + "unicode" +) + +type eventSignature struct { + Name string + Params []eventParameter +} + +type eventParameter struct { + Type string + Name string + Indexed bool +} + +func parseEventSignature(signature string) (eventSignature, error) { + signature = strings.TrimSpace(signature) + if signature == "" { + return eventSignature{}, fmt.Errorf("empty signature") + } + + if strings.HasPrefix(signature, "event") { + afterKeyword := signature[len("event"):] + if afterKeyword == "" { + return eventSignature{}, fmt.Errorf("event signature missing identifier and parameters") + } + + trimmed := strings.TrimLeftFunc(afterKeyword, unicode.IsSpace) + if len(trimmed) != len(afterKeyword) { + signature = strings.TrimSpace(trimmed) + } + } + + if strings.HasSuffix(signature, ";") { + signature = strings.TrimSpace(signature[:len(signature)-1]) + } + + if signature == "" { + return eventSignature{}, fmt.Errorf("event signature missing identifier and parameters") + } + + open := strings.Index(signature, "(") + close := strings.LastIndex(signature, ")") + if open == -1 || close == -1 || close < open { + return eventSignature{}, fmt.Errorf("invalid event signature format: %s", signature) + } + + name := strings.TrimSpace(signature[:open]) + if name == "" { + return eventSignature{}, fmt.Errorf("event name missing in signature: %s", signature) + } + + paramsRaw := signature[open+1 : close] + paramStrings, err := splitEventParameters(paramsRaw) + if err != nil { + return eventSignature{}, err + } + + params := make([]eventParameter, 0, len(paramStrings)) + for idx, paramString := range paramStrings { + paramString = strings.TrimSpace(paramString) + if paramString == "" { + continue + } + + param, err := parseEventParameter(paramString) + if err != nil { + return eventSignature{}, fmt.Errorf("parse param %d: %w", idx, err) + } + params = append(params, param) + } + + return eventSignature{Name: name, Params: params}, nil +} + +func splitEventParameters(params string) ([]string, error) { + if strings.TrimSpace(params) == "" { + return nil, nil + } + + result := []string{} + depth := 0 + last := 0 + for i, r := range params { + switch r { + case '(': + depth++ + case ')': + if depth == 0 { + return nil, fmt.Errorf("unbalanced parentheses in parameter list: %s", params) + } + depth-- + case ',': + if depth == 0 { + segment := strings.TrimSpace(params[last:i]) + result = append(result, segment) + last = i + 1 + } + } + } + + if depth != 0 { + return nil, fmt.Errorf("unbalanced parentheses in parameter list: %s", params) + } + + finalSegment := strings.TrimSpace(params[last:]) + if finalSegment != "" { + result = append(result, finalSegment) + } + + return result, nil +} + +func parseEventParameter(param string) (eventParameter, error) { + fields := strings.Fields(param) + if len(fields) == 0 { + return eventParameter{}, fmt.Errorf("empty parameter") + } + + result := eventParameter{Type: fields[0]} + idx := 1 + + if idx < len(fields) && fields[idx] == "payable" { + result.Type += " payable" + idx++ + } + + for idx < len(fields) && fields[idx] == "indexed" { + result.Indexed = true + idx++ + } + + if idx < len(fields) { + result.Name = fields[idx] + idx++ + } + + if idx < len(fields) { + return eventParameter{}, fmt.Errorf("unexpected tokens in parameter: %s", param) + } + + return result, nil +} diff --git a/internal/stylus/event_signature_test.go b/internal/stylus/event_signature_test.go new file mode 100644 index 0000000000..59e8aeaf97 --- /dev/null +++ b/internal/stylus/event_signature_test.go @@ -0,0 +1,230 @@ +package stylus + +import ( + "reflect" + "strings" + "testing" +) + +func TestParseEventSignatureBasicIndexed(t *testing.T) { + got, err := parseEventSignature("event Transfer(address indexed from, address indexed to, uint256 value);") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Transfer", + Params: []eventParameter{ + {Type: "address", Name: "from", Indexed: true}, + {Type: "address", Name: "to", Indexed: true}, + {Type: "uint256", Name: "value"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureTupleParam(t *testing.T) { + got, err := parseEventSignature("event Complex(tuple(uint256,string) indexed meta, uint8 flag);") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Complex", + Params: []eventParameter{ + {Type: "tuple(uint256,string)", Name: "meta", Indexed: true}, + {Type: "uint8", Name: "flag"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureIndexedWithoutName(t *testing.T) { + got, err := parseEventSignature("event Anonymous(uint256 indexed);") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Anonymous", + Params: []eventParameter{ + {Type: "uint256", Indexed: true}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureNoParams(t *testing.T) { + got, err := parseEventSignature("event Ping();") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Ping", + Params: []eventParameter{}, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureTrimsWhitespace(t *testing.T) { + got, err := parseEventSignature(" event EventWithSpaces ( address a , uint256 b ) ; ") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "EventWithSpaces", + Params: []eventParameter{ + {Type: "address", Name: "a"}, + {Type: "uint256", Name: "b"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureArrayTypes(t *testing.T) { + got, err := parseEventSignature("event WithArrays(address[] indexed owners, uint256[3] balances, bytes32[][] data);") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "WithArrays", + Params: []eventParameter{ + {Type: "address[]", Name: "owners", Indexed: true}, + {Type: "uint256[3]", Name: "balances"}, + {Type: "bytes32[][]", Name: "data"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignaturePayableAddress(t *testing.T) { + got, err := parseEventSignature("event WithPayable(address payable indexed recipient, address payable sender);") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "WithPayable", + Params: []eventParameter{ + {Type: "address payable", Name: "recipient", Indexed: true}, + {Type: "address payable", Name: "sender"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureMissingParentheses(t *testing.T) { + assertEventSignatureError(t, "event BrokenEvent;", "invalid event signature") +} + +func TestParseEventSignatureUnexpectedTokens(t *testing.T) { + assertEventSignatureError(t, "event Bad(uint256 value extra);", "unexpected tokens") +} + +func TestParseEventSignatureUnbalancedTuple(t *testing.T) { + assertEventSignatureError(t, "event Bad(tuple(uint256,string) value;", "unbalanced parentheses") +} + +func assertEventSignatureEqual(t *testing.T, got, want eventSignature) { + if !reflect.DeepEqual(got, want) { + t.Fatalf("parseEventSignature() = %#v, want %#v", got, want) + } +} + +func assertEventSignatureError(t *testing.T, signature, substr string) { + _, err := parseEventSignature(signature) + if err == nil { + t.Fatalf("expected error containing %q, got nil", substr) + } + if !strings.Contains(err.Error(), substr) { + t.Fatalf("expected error containing %q, got %v", substr, err) + } +} + +func TestParseEventSignatureNoPrefix(t *testing.T) { + got, err := parseEventSignature("Transfer(address indexed from, address indexed to, uint256 value);") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Transfer", + Params: []eventParameter{ + {Type: "address", Name: "from", Indexed: true}, + {Type: "address", Name: "to", Indexed: true}, + {Type: "uint256", Name: "value"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureNoSemicolon(t *testing.T) { + got, err := parseEventSignature("event Transfer(address indexed from, address indexed to, uint256 value)") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Transfer", + Params: []eventParameter{ + {Type: "address", Name: "from", Indexed: true}, + {Type: "address", Name: "to", Indexed: true}, + {Type: "uint256", Name: "value"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureNoPrefixOrSemicolon(t *testing.T) { + got, err := parseEventSignature("Transfer(address indexed from, address indexed to, uint256 value)") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "Transfer", + Params: []eventParameter{ + {Type: "address", Name: "from", Indexed: true}, + {Type: "address", Name: "to", Indexed: true}, + {Type: "uint256", Name: "value"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} + +func TestParseEventSignatureBareEventKeyword(t *testing.T) { + assertEventSignatureError(t, "event", "missing identifier and parameters") +} + +func TestParseEventSignatureNestedTupleArray(t *testing.T) { + sig := "\treplaceDeposit((bytes4,bytes2,bytes,bytes,bytes,bytes4)[],(bytes,uint256,uint256),uint256,bytes32)" + got, err := parseEventSignature(sig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := eventSignature{ + Name: "replaceDeposit", + Params: []eventParameter{ + {Type: "(bytes4,bytes2,bytes,bytes,bytes,bytes4)[]"}, + {Type: "(bytes,uint256,uint256)"}, + {Type: "uint256"}, + {Type: "bytes32"}, + }, + } + + assertEventSignatureEqual(t, got, want) +} diff --git a/internal/stylus/stylus.go b/internal/stylus/stylus.go index 6a8ae247c9..eabd8dfa14 100644 --- a/internal/stylus/stylus.go +++ b/internal/stylus/stylus.go @@ -3,24 +3,34 @@ package stylus import ( "context" "encoding/json" + "fmt" "os" "github.com/metacraft-labs/trace_record" "github.com/tetratelabs/wazero" ) -func Instantiate(ctx context.Context, r wazero.Runtime, stylusTracePath string, record *trace_record.TraceRecord) (*StylusTrace, error) { +func Instantiate(ctx context.Context, r wazero.Runtime, stylusTracePath string, stylusSignatureMapPath string, record *trace_record.TraceRecord) (*StylusTrace, error) { + stylusState := StylusTrace{} + stylusTraceJson, err := os.ReadFile(stylusTracePath) if err != nil { return nil, err } - stylusState := StylusTrace{} - if err := json.Unmarshal(stylusTraceJson, &stylusState.events); err != nil { return nil, err } + stylusSignatureMapJson, err := os.ReadFile(stylusSignatureMapPath) + if err != nil { + fmt.Printf("Can't read signature map. Error: %v\n", err) + } + + if err := json.Unmarshal(stylusSignatureMapJson, &stylusState.eventSignatureMap); err != nil { + fmt.Printf("Can't parse signature map. Error: %v\n", err) + } + moduleBuilder := r.NewHostModuleBuilder("vm_hooks") moduleBuilder = exportSylusFunctions(moduleBuilder, &stylusState, record) diff --git a/internal/stylus/stylus_funcs.go b/internal/stylus/stylus_funcs.go index 56c7bed2b0..4656dcdd6b 100644 --- a/internal/stylus/stylus_funcs.go +++ b/internal/stylus/stylus_funcs.go @@ -4,6 +4,8 @@ import ( "context" "encoding/binary" "fmt" + "math" + "strings" "github.com/metacraft-labs/trace_record" "github.com/tetratelabs/wazero" @@ -376,12 +378,90 @@ func exportEmitLog(mb wazero.HostModuleBuilder, trace *StylusTrace, record *trac func(m api.Module, stack []uint64, event evmEvent) { mem := m.Memory() dataPtr := uint32(stack[0]) - len := uint32(stack[1]) - data := readMemoryBytes(mem, dataPtr, len) + rawDataLen := uint32(stack[1]) + numTopics := stack[2] + + rawData := readMemoryBytes(mem, dataPtr, rawDataLen) _ = event - // TODO: convert this to human readable format - record.RegisterRecordEvent(trace_record.EventKindEvmEvent, "emit_log", hexBytes(data)) + requestedTopics := numTopics + if requestedTopics > uint64(math.MaxInt) { + requestedTopics = uint64(math.MaxInt) + } + + topicCount := int(requestedTopics) + maxTopics := len(rawData) / 32 + if topicCount > maxTopics { + topicCount = maxTopics + } + + topics := make([][]byte, topicCount) + for i := 0; i < topicCount; i++ { + start := i * 32 + topics[i] = rawData[start : start+32] + } + + data := rawData[topicCount*32:] + + var builder strings.Builder + var warnings []string + + if topicCount > 0 && trace != nil && trace.eventSignatureMap != nil { + if signature, hasSignature := trace.eventSignatureMap[hexBytes(topics[0])]; hasSignature { + parsed, err := parseEventSignature(signature) + if err != nil { + builder.WriteString(signature) + warnings = append(warnings, fmt.Sprintf("failed to parse event signature: %v", err)) + } else { + builder.WriteString(parsed.Name) + values, decodeWarnings := decodeEventParameters(parsed, topics[1:], data) + warnings = append(warnings, decodeWarnings...) + for i, pv := range values { + value := pv.value + if value == "" { + value = "" + } + + label := parameterLabel(pv.param, i) + prefix := "" + if pv.param.Indexed { + prefix = "indexed " + } + + builder.WriteString(fmt.Sprintf("\n%s%s = %s", prefix, label, value)) + if pv.warning != "" { + builder.WriteString(fmt.Sprintf(" (%s)", pv.warning)) + } + } + } + } + } + + if builder.Len() == 0 { + if topicCount > 0 { + builder.WriteString(hexBytes(topics[0])) + } else { + builder.WriteString("emit_log") + } + } + + if topicCount < int(requestedTopics) { + warnings = append(warnings, fmt.Sprintf("requested %d topics but payload exposes %d", numTopics, topicCount)) + } + + for _, warn := range warnings { + builder.WriteString(fmt.Sprintf("\n! %s", warn)) + } + + for i := 0; i < topicCount; i++ { + builder.WriteString(fmt.Sprintf("\nTOPIC[%d] = %s", i, hexBytes(topics[i]))) + } + + if len(data) > 0 { + builder.WriteString(fmt.Sprintf("\nDATA = %s", hexBytes(data))) + } + + record.RegisterRecordEvent(trace_record.EventKindEvmEvent, "emit_log", builder.String()) }) } diff --git a/internal/stylus/stylus_trace.go b/internal/stylus/stylus_trace.go index a4650080fc..a8508cb4d5 100644 --- a/internal/stylus/stylus_trace.go +++ b/internal/stylus/stylus_trace.go @@ -17,7 +17,9 @@ type evmEvent struct { func (e *evmEvent) UnmarshalJSON(data []byte) error { var obj map[string]interface{} - json.Unmarshal(data, &obj) + if err := json.Unmarshal(data, &obj); err != nil { + return fmt.Errorf("parse stylus event: %w", err) + } var ok bool e.name, ok = obj["name"].(string) @@ -65,6 +67,8 @@ func (e *evmEvent) UnmarshalJSON(data []byte) error { type StylusTrace struct { events []evmEvent current int + + eventSignatureMap map[string]string } func (st *StylusTrace) nextEvent(event string) (evmEvent, error) {