From 4bb7e8cbf0da45c85b70532dc73791e0e2e1d78c Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Wed, 19 Jul 2023 08:59:40 +0100 Subject: [PATCH] fix(protocol-parser): properly decode empty records (#1177) Co-authored-by: alvarius --- .changeset/itchy-kids-relax.md | 5 +++++ packages/protocol-parser/src/decodeKeyTuple.ts | 5 +++++ .../protocol-parser/src/decodeRecord.test.ts | 11 +++++++++++ packages/protocol-parser/src/decodeRecord.ts | 17 +++++++++++++---- .../protocol-parser/src/hexToSchema.test.ts | 16 ++++++++++++++++ packages/protocol-parser/src/hexToSchema.ts | 3 +++ 6 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 .changeset/itchy-kids-relax.md diff --git a/.changeset/itchy-kids-relax.md b/.changeset/itchy-kids-relax.md new file mode 100644 index 0000000000..efe5c84533 --- /dev/null +++ b/.changeset/itchy-kids-relax.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/protocol-parser": patch +--- + +`decodeRecord` now properly decodes empty records diff --git a/packages/protocol-parser/src/decodeKeyTuple.ts b/packages/protocol-parser/src/decodeKeyTuple.ts index 554068ea4d..81342f13d4 100644 --- a/packages/protocol-parser/src/decodeKeyTuple.ts +++ b/packages/protocol-parser/src/decodeKeyTuple.ts @@ -5,6 +5,11 @@ import { Schema } from "./common"; // key tuples are encoded in the same way as abi.encode, so we can decode them with viem export function decodeKeyTuple(keySchema: Schema, keyTuple: readonly Hex[]): StaticPrimitiveType[] { + if (keySchema.staticFields.length !== keyTuple.length) { + throw new Error( + `key tuple length ${keyTuple.length} does not match key schema length ${keySchema.staticFields.length}` + ); + } return keyTuple.map( (key, index) => decodeAbiParameters([{ type: keySchema.staticFields[index] }], key)[0] as StaticPrimitiveType ); diff --git a/packages/protocol-parser/src/decodeRecord.test.ts b/packages/protocol-parser/src/decodeRecord.test.ts index c201ba08e1..87016e3891 100644 --- a/packages/protocol-parser/src/decodeRecord.test.ts +++ b/packages/protocol-parser/src/decodeRecord.test.ts @@ -10,4 +10,15 @@ describe("decodeRecord", () => { ); expect(values).toStrictEqual([1, 2n, [3, 4], "some string"]); }); + + it("can decode an empty record", () => { + const schema = { staticFields: [], dynamicFields: ["string", "string"] } as const; + const values = decodeRecord(schema, "0x0000000000000000000000000000000000000000000000000000000000000000"); + expect(values).toMatchInlineSnapshot(` + [ + "", + "", + ] + `); + }); }); diff --git a/packages/protocol-parser/src/decodeRecord.ts b/packages/protocol-parser/src/decodeRecord.ts index d1b65eb401..6e1a4fb46e 100644 --- a/packages/protocol-parser/src/decodeRecord.ts +++ b/packages/protocol-parser/src/decodeRecord.ts @@ -1,4 +1,9 @@ -import { StaticPrimitiveType, DynamicPrimitiveType, staticAbiTypeToByteLength } from "@latticexyz/schema-type"; +import { + StaticPrimitiveType, + DynamicPrimitiveType, + staticAbiTypeToByteLength, + dynamicAbiTypeToDefaultValue, +} from "@latticexyz/schema-type"; import { Hex, sliceHex } from "viem"; import { Schema } from "./common"; import { decodeDynamicField } from "./decodeDynamicField"; @@ -37,9 +42,13 @@ export function decodeRecord(schema: Schema, data: Hex): readonly (StaticPrimiti schema.dynamicFields.forEach((fieldType, i) => { const dataLength = dataLayout.fieldByteLengths[i]; - const value = decodeDynamicField(fieldType, sliceHex(data, bytesOffset, bytesOffset + dataLength)); - bytesOffset += dataLength; - values.push(value); + if (dataLength > 0) { + const value = decodeDynamicField(fieldType, sliceHex(data, bytesOffset, bytesOffset + dataLength)); + bytesOffset += dataLength; + values.push(value); + } else { + values.push(dynamicAbiTypeToDefaultValue[fieldType]); + } }); // Warn user if dynamic data length doesn't match the schema, because data corruption might be possible. diff --git a/packages/protocol-parser/src/hexToSchema.test.ts b/packages/protocol-parser/src/hexToSchema.test.ts index 37c97fdc34..f978a8d7b6 100644 --- a/packages/protocol-parser/src/hexToSchema.test.ts +++ b/packages/protocol-parser/src/hexToSchema.test.ts @@ -35,6 +35,22 @@ describe("hexToSchema", () => { ], } `); + + expect(hexToSchema("0x00570800616100030700001f0000000000000000000000000000000000000000")).toMatchInlineSnapshot(` + { + "dynamicFields": [], + "staticFields": [ + "address", + "address", + "uint8", + "uint32", + "uint64", + "uint8", + "uint8", + "uint256", + ], + } + `); }); it("throws if schema hex data is not bytes32", () => { diff --git a/packages/protocol-parser/src/hexToSchema.ts b/packages/protocol-parser/src/hexToSchema.ts index a07ed85ab8..b56897f91c 100644 --- a/packages/protocol-parser/src/hexToSchema.ts +++ b/packages/protocol-parser/src/hexToSchema.ts @@ -26,6 +26,9 @@ export function hexToSchema(data: Hex): Schema { // validate static data length const actualStaticDataLength = staticFields.reduce((acc, fieldType) => acc + staticAbiTypeToByteLength[fieldType], 0); if (actualStaticDataLength !== staticDataLength) { + console.warn( + `Schema "${data}" static data length (${staticDataLength}) did not match the summed length of all static fields (${actualStaticDataLength}). Is \`staticAbiTypeToByteLength\` up to date with Solidity schema types?` + ); throw new SchemaStaticLengthMismatchError(data, staticDataLength, actualStaticDataLength); }