diff --git a/src/evm/SolidityMetadata.test.ts b/src/evm/SolidityMetadata.test.ts index ff6c76bf9a..b10213ee4c 100644 --- a/src/evm/SolidityMetadata.test.ts +++ b/src/evm/SolidityMetadata.test.ts @@ -119,6 +119,9 @@ it("decodes distinct byte keys and distinct nested-container keys", () => { expect(inspectSolidityMetadata(bytecode("a2810101810202"))).toMatchObject({ status: "partial", }); + expect(inspectSolidityMetadata(bytecode("a281800181a002"))).toMatchObject({ + status: "partial", + }); }); it.each([ @@ -128,6 +131,9 @@ it.each([ ["floating point", "a16178fa3f800000"], ["duplicate key", "a2617801617802"], ["duplicate byte key", "a2410101410102"], + ["duplicate array key", "a280018002"], + ["duplicate map key", "a2a001a002"], + ["duplicate map key in another order", "a2a20102030401a20304010202"], ["excessive nesting", "a16178" + "81".repeat(65) + "00"], ["impossible collection", "a161788a00"], ])("keeps exact ranges and unavailable interpretation for %s", (_name, hex) => { @@ -139,6 +145,18 @@ it.each([ }); }); +it("rejects repeated deeply nested container keys promptly", () => { + const key = "81".repeat(62) + "00"; + const started = performance.now(); + expect( + inspectSolidityMetadata(bytecode("a2" + key + "01" + key + "02")), + ).toMatchObject({ + status: "not-decoded", + diagnostic: expect.stringContaining("found repeat map key"), + }); + expect(performance.now() - started).toBeLessThan(1000); +}); + it.each(["", "60006000f3", "000000", "00010001"])( "does not claim absent Solidity metadata for an unavailable trailer: %s", (hex) => diff --git a/src/evm/SolidityMetadata.ts b/src/evm/SolidityMetadata.ts index b5cdfb64b1..ab92c97ea5 100644 --- a/src/evm/SolidityMetadata.ts +++ b/src/evm/SolidityMetadata.ts @@ -90,6 +90,17 @@ const scalar = (value: unknown): SolidityMetadataScalar => { ); }; +/** Value identity for duplicate-key checks; map entry order is not significant. */ +const keyIdentity = (value: unknown): string => { + if (value instanceof Map) + return `{${[...value] + .map(([key, item]) => `${keyIdentity(key)}:${keyIdentity(item)}`) + .sort() + .join(",")}}`; + if (Array.isArray(value)) return `[${value.map(keyIdentity).join(",")}]`; + return JSON.stringify(scalar(value)); +}; + /** Parse only a plausible terminal root-map candidate; never establish absence. */ export const inspectSolidityMetadata = ( bytes: Uint8Array, @@ -122,20 +133,15 @@ export const inspectSolidityMetadata = ( }); if (!(raw instanceof Map)) throw new Error("Terminal CBOR candidate did not decode to a root map."); - // cborg compares Map keys by identity, so repeated byte-string keys (each - // its own Uint8Array) pass its duplicate check; compare scalars by value. + // 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(); for (const [key, value] of raw as Map) { - const entry = { key: scalar(key), value: scalar(value) }; - if (entry.key.kind !== "uninterpreted") { - const identity = JSON.stringify(entry.key); - if (seen.has(identity)) - throw new Error( - `CBOR decode error: found repeat map key ${identity}`, - ); - seen.add(identity); - } - result.entries.push(entry); + 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( ({ key, value }) =>