diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md index 6ee6029cf3..781112bb6f 100644 --- a/docs/evm-bytecode.md +++ b/docs/evm-bytecode.md @@ -45,7 +45,8 @@ license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) map from the selected bytes. Integers use decimal strings; byte strings use hex, and scalar keys retain their types. The recorded offset/length addresses the original bytecode, so summary delivery does not copy an opaque CBOR payload. - Nested containers have partial scalar coverage; unsupported or malformed + Nested containers have partial scalar coverage; map keys are still checked for + duplicates throughout nested maps and arrays. Unsupported or malformed candidates report `not-decoded` with a diagnostic. Decoding is bounded to 64 container levels and the terminal payload's encoded size. - An unavailable trailer does not prove metadata absence or Solidity origin. diff --git a/src/evm/SolidityMetadata.test.ts b/src/evm/SolidityMetadata.test.ts index b10213ee4c..8d9fade9a6 100644 --- a/src/evm/SolidityMetadata.test.ts +++ b/src/evm/SolidityMetadata.test.ts @@ -122,6 +122,12 @@ it("decodes distinct byte keys and distinct nested-container keys", () => { expect(inspectSolidityMetadata(bytecode("a281800181a002"))).toMatchObject({ status: "partial", }); + expect( + inspectSolidityMetadata(bytecode("a264736f6c634300081e6178a2410101410202")), + ).toMatchObject({ + status: "partial", + compiler_version: { representation: "release-bytes", value: "0.8.30" }, + }); }); it.each([ @@ -131,6 +137,11 @@ it.each([ ["floating point", "a16178fa3f800000"], ["duplicate key", "a2617801617802"], ["duplicate byte key", "a2410101410102"], + [ + "duplicate byte key in nested map", + "a264736f6c634300081e6178a2410101410102", + ], + ["duplicate byte key in map inside array", "a1617881a2410101410102"], ["duplicate array key", "a280018002"], ["duplicate map key", "a2a001a002"], ["duplicate map key in another order", "a2a20102030401a20304010202"], diff --git a/src/evm/SolidityMetadata.ts b/src/evm/SolidityMetadata.ts index ab92c97ea5..0afd626358 100644 --- a/src/evm/SolidityMetadata.ts +++ b/src/evm/SolidityMetadata.ts @@ -101,6 +101,23 @@ const keyIdentity = (value: unknown): string => { return JSON.stringify(scalar(value)); }; +/** Check every map boundary, even when nested values remain uninterpreted. */ +const validateMapKeys = (value: unknown): void => { + if (value instanceof Map) { + const seen = new Set(); + for (const [key, item] of value) { + const identity = keyIdentity(key); + if (seen.has(identity)) + throw new Error(`CBOR decode error: found repeat map key ${identity}`); + seen.add(identity); + validateMapKeys(key); + validateMapKeys(item); + } + } else if (Array.isArray(value)) { + for (const item of value) validateMapKeys(item); + } +}; + /** Parse only a plausible terminal root-map candidate; never establish absence. */ export const inspectSolidityMetadata = ( bytes: Uint8Array, @@ -133,14 +150,11 @@ export const inspectSolidityMetadata = ( }); if (!(raw instanceof Map)) throw new Error("Terminal CBOR candidate did not decode to a root map."); + validateMapKeys(raw); // cborg compares Map keys by identity, so repeated byte-string or container - // keys (each its own object) pass its duplicate check; compare by value. - const seen = new Set(); + // keys (each its own object) pass its duplicate check; compare by value at + // every map boundary before projecting any entries. for (const [key, value] of raw as Map) { - const identity = keyIdentity(key); - if (seen.has(identity)) - throw new Error(`CBOR decode error: found repeat map key ${identity}`); - seen.add(identity); result.entries.push({ key: scalar(key), value: scalar(value) }); } result.status = result.entries.some( diff --git a/tests/boundary/evm/solidityMetadataWorker.test.ts b/tests/boundary/evm/solidityMetadataWorker.test.ts index df72002b5d..cd68439df8 100644 --- a/tests/boundary/evm/solidityMetadataWorker.test.ts +++ b/tests/boundary/evm/solidityMetadataWorker.test.ts @@ -13,6 +13,21 @@ it.each([ ["integer", "00a1011bffffffffffffffff000b", "decoded"], ["BOM text", "00a1617864efbbbf610008", "decoded"], ["nested value", "00a1617881000005", "partial"], + [ + "distinct nested byte keys", + "00a264736f6c634300081e6178a24101014102020013", + "partial", + ], + [ + "duplicate nested byte keys", + "00a264736f6c634300081e6178a24101014101020013", + "not-decoded", + ], + [ + "duplicate nested text keys", + "00a264736f6c634300081e6178a26161016161020013", + "not-decoded", + ], ["unavailable trailer", "00", "unavailable"], ["unsupported tag", "00a16178c0000005", "not-decoded"], ["floating point", "00a16178fa3f8000000008", "not-decoded"], @@ -71,6 +86,18 @@ it.each([ kind: "uninterpreted", container: "array", }); + if (kind === "distinct nested byte keys") { + expect(metadata.compiler_version?.value).toBe("0.8.30"); + expect(metadata.bytecode_offset).toBe(1); + expect(metadata.cbor_bytes).toBe(19); + } + if (kind.startsWith("duplicate nested")) { + expect(metadata.entries).toEqual([]); + expect(metadata.compiler_version).toBeNull(); + expect(metadata.diagnostic).toContain("repeat map key"); + expect(metadata.bytecode_offset).toBe(1); + expect(metadata.cbor_bytes).toBe(19); + } if (kind === "unavailable trailer") { expect(metadata.bytecode_offset).toBeNull(); expect(metadata.entries).toEqual([]);