From 433078c54c22fa1b4e32d7204fb41bd5f79ca1db Mon Sep 17 00:00:00 2001 From: dk1a Date: Fri, 18 Aug 2023 18:50:45 +0300 Subject: [PATCH] refactor(store): optimize PackedCounter (#1231) Co-authored-by: alvarius Co-authored-by: Kevin Ingersoll --- .changeset/little-ravens-yawn.md | 14 ++ .../src/codegen/tables/NumberList.sol | 8 +- .../src/codegen/tables/MessageTable.sol | 8 +- .../src/codegen/tables/Dynamics1.sol | 18 +- .../src/codegen/tables/Dynamics2.sol | 10 +- .../src/codegen/tables/Singleton.sol | 10 +- .../protocol-parser/src/decodeRecord.test.ts | 2 +- .../src/hexToPackedCounter.test.ts | 6 +- .../protocol-parser/src/hexToPackedCounter.ts | 10 +- .../services/pkg/mode/storecore/encoding.go | 2 +- .../pkg/mode/storecore/encoding_test.go | 2 +- .../pkg/protocol-parser/decodeRecord_test.go | 2 +- .../pkg/protocol-parser/hexToPackedCounter.go | 8 +- .../hexToPackedCounter_test.go | 4 +- .../store-sync/src/blockLogsToStorage.test.ts | 4 +- .../src/sqlite/sqliteStorage.test.ts | 4 +- .../MirrorSubscriber.abi.json | 11 ++ .../PackedCounterInstance.abi.json | 13 ++ .../abi/StoreMock.sol/StoreMock.abi.json | 11 ++ packages/store/gas-report.json | 164 ++++++++-------- packages/store/src/PackedCounter.sol | 176 ++++++++++-------- .../store/src/codegen/tables/Callbacks.sol | 8 +- packages/store/src/codegen/tables/Hooks.sol | 8 +- packages/store/src/codegen/tables/Mixed.sol | 9 +- packages/store/src/codegen/tables/Tables.sol | 9 +- packages/store/test/Mixed.t.sol | 2 +- packages/store/test/PackedCounter.t.sol | 98 +++++++--- packages/store/test/StoreCore.t.sol | 10 +- packages/store/test/StoreCoreGas.t.sol | 22 +-- packages/store/ts/codegen/renderTable.ts | 24 ++- .../AccessManagementSystem.abi.json | 11 ++ .../abi/CoreModule.sol/CoreModule.abi.json | 11 ++ .../abi/CoreSystem.sol/CoreSystem.abi.json | 11 ++ .../KeysInTableHook.abi.json | 11 ++ .../KeysWithValueHook.abi.json | 11 ++ .../KeysWithValueModule.abi.json | 11 ++ .../PackedCounterInstance.abi.json | 13 ++ .../StoreRegistrationSystem.abi.json | 11 ++ .../UniqueEntitySystem.abi.json | 11 ++ packages/world/abi/World.sol/World.abi.json | 11 ++ .../WorldRegistrationSystem.abi.json | 11 ++ .../PackedCounterInstance.abi.json | 13 ++ packages/world/gas-report.json | 98 +++++----- .../src/modules/core/tables/SystemHooks.sol | 8 +- .../keysintable/tables/KeysInTable.sol | 18 +- .../keyswithvalue/tables/KeysWithValue.sol | 8 +- packages/world/test/tables/AddressArray.sol | 8 +- 47 files changed, 614 insertions(+), 339 deletions(-) create mode 100644 .changeset/little-ravens-yawn.md create mode 100644 packages/store/abi/PackedCounter.sol/PackedCounterInstance.abi.json create mode 100644 packages/world/abi/PackedCounter.sol/PackedCounterInstance.abi.json create mode 100644 packages/world/abi/src/PackedCounter.sol/PackedCounterInstance.abi.json diff --git a/.changeset/little-ravens-yawn.md b/.changeset/little-ravens-yawn.md new file mode 100644 index 0000000000..b099e16722 --- /dev/null +++ b/.changeset/little-ravens-yawn.md @@ -0,0 +1,14 @@ +--- +"@latticexyz/cli": patch +"@latticexyz/protocol-parser": major +"@latticexyz/services": major +"@latticexyz/store-sync": major +"@latticexyz/store": major +"@latticexyz/world": patch +--- + +Reverse PackedCounter encoding, to optimize gas for bitshifts. +Ints are right-aligned, shifting using an index is straightforward if they are indexed right-to-left. + +- Previous encoding: (7 bytes | accumulator),(5 bytes | counter 1),...,(5 bytes | counter 5) +- New encoding: (5 bytes | counter 5),...,(5 bytes | counter 1),(7 bytes | accumulator) diff --git a/e2e/packages/contracts/src/codegen/tables/NumberList.sol b/e2e/packages/contracts/src/codegen/tables/NumberList.sol index 049c84f70a..dbda775b36 100644 --- a/e2e/packages/contracts/src/codegen/tables/NumberList.sol +++ b/e2e/packages/contracts/src/codegen/tables/NumberList.sol @@ -194,9 +194,11 @@ library NumberList { /** Tightly pack full data using this table's schema */ function encode(uint32[] memory value) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(value.length * 4); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(value.length * 4); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); } diff --git a/examples/minimal/packages/contracts/src/codegen/tables/MessageTable.sol b/examples/minimal/packages/contracts/src/codegen/tables/MessageTable.sol index 402068d07e..928fed1d72 100644 --- a/examples/minimal/packages/contracts/src/codegen/tables/MessageTable.sol +++ b/examples/minimal/packages/contracts/src/codegen/tables/MessageTable.sol @@ -77,9 +77,11 @@ library MessageTable { /** Tightly pack full data using this table's schema */ function encode(string memory value) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(bytes(value).length); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(bytes(value).length); + } return abi.encodePacked(_encodedLengths.unwrap(), bytes((value))); } diff --git a/packages/cli/contracts/src/codegen/tables/Dynamics1.sol b/packages/cli/contracts/src/codegen/tables/Dynamics1.sol index cc7288b5a7..49b381771c 100644 --- a/packages/cli/contracts/src/codegen/tables/Dynamics1.sol +++ b/packages/cli/contracts/src/codegen/tables/Dynamics1.sol @@ -998,13 +998,17 @@ library Dynamics1 { address[4] memory staticAddrs, bool[5] memory staticBools ) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](5); - _counters[0] = uint40(staticB32.length * 32); - _counters[1] = uint40(staticI32.length * 4); - _counters[2] = uint40(staticU128.length * 16); - _counters[3] = uint40(staticAddrs.length * 20); - _counters[4] = uint40(staticBools.length * 1); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack( + staticB32.length * 32, + staticI32.length * 4, + staticU128.length * 16, + staticAddrs.length * 20, + staticBools.length * 1 + ); + } return abi.encodePacked( diff --git a/packages/cli/contracts/src/codegen/tables/Dynamics2.sol b/packages/cli/contracts/src/codegen/tables/Dynamics2.sol index 2a131c165a..79272c9e40 100644 --- a/packages/cli/contracts/src/codegen/tables/Dynamics2.sol +++ b/packages/cli/contracts/src/codegen/tables/Dynamics2.sol @@ -598,11 +598,11 @@ library Dynamics2 { /** Tightly pack full data using this table's schema */ function encode(uint64[] memory u64, string memory str, bytes memory b) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](3); - _counters[0] = uint40(u64.length * 8); - _counters[1] = uint40(bytes(str).length); - _counters[2] = uint40(bytes(b).length); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(u64.length * 8, bytes(str).length, bytes(b).length); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((u64)), bytes((str)), bytes((b))); } diff --git a/packages/cli/contracts/src/codegen/tables/Singleton.sol b/packages/cli/contracts/src/codegen/tables/Singleton.sol index 009f0ce7ee..f3632fbf3d 100644 --- a/packages/cli/contracts/src/codegen/tables/Singleton.sol +++ b/packages/cli/contracts/src/codegen/tables/Singleton.sol @@ -577,11 +577,11 @@ library Singleton { uint32[2] memory v3, uint32[1] memory v4 ) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](3); - _counters[0] = uint40(v2.length * 4); - _counters[1] = uint40(v3.length * 4); - _counters[2] = uint40(v4.length * 4); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(v2.length * 4, v3.length * 4, v4.length * 4); + } return abi.encodePacked( diff --git a/packages/protocol-parser/src/decodeRecord.test.ts b/packages/protocol-parser/src/decodeRecord.test.ts index 87016e3891..a5bca516e6 100644 --- a/packages/protocol-parser/src/decodeRecord.test.ts +++ b/packages/protocol-parser/src/decodeRecord.test.ts @@ -6,7 +6,7 @@ describe("decodeRecord", () => { const schema = { staticFields: ["uint32", "uint128"], dynamicFields: ["uint32[]", "string"] } as const; const values = decodeRecord( schema, - "0x0000000100000000000000000000000000000002000000000000130000000008000000000b0000000000000000000000000000000000000300000004736f6d6520737472696e67" + "0x0000000100000000000000000000000000000002000000000000000000000000000000000000000b0000000008000000000000130000000300000004736f6d6520737472696e67" ); expect(values).toStrictEqual([1, 2n, [3, 4], "some string"]); }); diff --git a/packages/protocol-parser/src/hexToPackedCounter.test.ts b/packages/protocol-parser/src/hexToPackedCounter.test.ts index db8b8c3893..dc98665345 100644 --- a/packages/protocol-parser/src/hexToPackedCounter.test.ts +++ b/packages/protocol-parser/src/hexToPackedCounter.test.ts @@ -3,7 +3,7 @@ import { hexToPackedCounter } from "./hexToPackedCounter"; describe("hexToPackedCounter", () => { it("decodes hex data to packed counter", () => { - expect(hexToPackedCounter("0x0000000000008000000000200000000020000000004000000000000000000000")) + expect(hexToPackedCounter("0x0000000000000000000000000000400000000020000000002000000000000080")) .toMatchInlineSnapshot(` { "fieldByteLengths": [ @@ -26,9 +26,9 @@ describe("hexToPackedCounter", () => { it("throws if packed counter total byte length doesn't match summed byte length of fields", () => { expect(() => - hexToPackedCounter("0x0000000000004000000000200000000020000000004000000000000000000000") + hexToPackedCounter("0x0000000000000000000000000000400000000020000000002000000000000040") ).toThrowErrorMatchingInlineSnapshot( - '"PackedCounter \\"0x0000000000004000000000200000000020000000004000000000000000000000\\" total bytes length (64) did not match the summed length of all field byte lengths (128)."' + '"PackedCounter \\"0x0000000000000000000000000000400000000020000000002000000000000040\\" total bytes length (64) did not match the summed length of all field byte lengths (128)."' ); }); }); diff --git a/packages/protocol-parser/src/hexToPackedCounter.ts b/packages/protocol-parser/src/hexToPackedCounter.ts index 5ab6cf3ac2..4a244e0cf5 100644 --- a/packages/protocol-parser/src/hexToPackedCounter.ts +++ b/packages/protocol-parser/src/hexToPackedCounter.ts @@ -5,8 +5,8 @@ import { InvalidHexLengthForPackedCounterError, PackedCounterLengthMismatchError // Keep this logic in sync with PackedCounter.sol -// - First 7 bytes (uint56) are used for the total byte length of the dynamic data -// - The next 5 byte (uint40) sections are used for the byte length of each field, in the same order as the schema's dynamic fields +// - Last 7 bytes (uint56) are used for the total byte length of the dynamic data +// - The next 5 byte (uint40) sections are used for the byte length of each field, indexed from right to left // We use byte lengths rather than item counts so that, on chain, we can slice without having to get the schema first (and thus the field lengths of each dynamic type) @@ -17,10 +17,12 @@ export function hexToPackedCounter(data: Hex): { if (data.length !== 66) { throw new InvalidHexLengthForPackedCounterError(data); } - const totalByteLength = decodeStaticField("uint56", sliceHex(data, 0, 7)); + const totalByteLength = decodeStaticField("uint56", sliceHex(data, 32 - 7, 32)); // TODO: use schema to make sure we only parse as many as we need (rather than zeroes at the end)? - const fieldByteLengths = decodeDynamicField("uint40[]", sliceHex(data, 7)); + const reversedFieldByteLengths = decodeDynamicField("uint40[]", sliceHex(data, 0, 32 - 7)); + // Reverse the lengths + const fieldByteLengths = Object.freeze([...reversedFieldByteLengths].reverse()); const summedLength = BigInt(fieldByteLengths.reduce((total, length) => total + length, 0)); if (summedLength !== totalByteLength) { diff --git a/packages/services/pkg/mode/storecore/encoding.go b/packages/services/pkg/mode/storecore/encoding.go index e04ffe7902..a4ad32a181 100644 --- a/packages/services/pkg/mode/storecore/encoding.go +++ b/packages/services/pkg/mode/storecore/encoding.go @@ -524,7 +524,7 @@ func (schema *Schema) DecodeFieldData(encoding []byte) *DecodedData { // since all uints are handled the same to handle those > Go uint64. dataLengthString, _ := packedCounterCounterType.DecodeStaticField( dynamicDataSlice, - packedCounterAccumulatorType.StaticByteLength()+uint64(i)*packedCounterCounterType.StaticByteLength(), + 32-packedCounterAccumulatorType.StaticByteLength()-uint64(i+1)*packedCounterCounterType.StaticByteLength(), ).(string) // Convert the length to a uint64. diff --git a/packages/services/pkg/mode/storecore/encoding_test.go b/packages/services/pkg/mode/storecore/encoding_test.go index 1aacbeaa2d..e438b67067 100644 --- a/packages/services/pkg/mode/storecore/encoding_test.go +++ b/packages/services/pkg/mode/storecore/encoding_test.go @@ -9,7 +9,7 @@ import ( func TestDecodeData(t *testing.T) { //nolint:lll // test - encoding := "0x000000000000ac000000000c00000000a00000000000000000000000000000004d6573736167655461626c65000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000" + encoding := "0x00000000000000000000000000000000000000a0000000000c000000000000ac4d6573736167655461626c65000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000" schema := &Schema{ Static: []SchemaType{}, Dynamic: []SchemaType{ diff --git a/packages/services/pkg/protocol-parser/decodeRecord_test.go b/packages/services/pkg/protocol-parser/decodeRecord_test.go index e0018db9ef..92f3f77921 100644 --- a/packages/services/pkg/protocol-parser/decodeRecord_test.go +++ b/packages/services/pkg/protocol-parser/decodeRecord_test.go @@ -22,7 +22,7 @@ func TestDecodeRecord(t *testing.T) { }, } - hex := "0x0000000100000000000000000000000000000002000000000000130000000008000000000b0000000000000000000000000000000000000300000004736f6d6520737472696e67" + hex := "0x0000000100000000000000000000000000000002000000000000000000000000000000000000000b0000000008000000000000130000000300000004736f6d6520737472696e67" expectedDecodedData := []interface{}{ uint32(1), diff --git a/packages/services/pkg/protocol-parser/hexToPackedCounter.go b/packages/services/pkg/protocol-parser/hexToPackedCounter.go index 49132ea044..3f6c5c190d 100644 --- a/packages/services/pkg/protocol-parser/hexToPackedCounter.go +++ b/packages/services/pkg/protocol-parser/hexToPackedCounter.go @@ -17,8 +17,12 @@ func HexToPackedCounter(data string) PackedCounter { panic(ErrInvalidHexLengthForPackedCounter) } - totalByteLength := DecodeStaticField(schematype.UINT56, HexSlice(data, 0, 7)).(uint64) - fieldByteLengths := convert.ToUint64Array(DecodeDynamicField(schematype.UINT40_ARRAY, HexSliceFrom(data, 7))) + totalByteLength := DecodeStaticField(schematype.UINT56, HexSlice(data, 32-7, 32)).(uint64) + fieldByteLengths := convert.ToUint64Array(DecodeDynamicField(schematype.UINT40_ARRAY, HexSlice(data, 0, 32-7))) + // Reverse the lengths + for i, j := 0, len(fieldByteLengths)-1; i < j; i, j = i+1, j-1 { + fieldByteLengths[i], fieldByteLengths[j] = fieldByteLengths[j], fieldByteLengths[i] + } summedLength := new(big.Int) for _, length := range fieldByteLengths { diff --git a/packages/services/pkg/protocol-parser/hexToPackedCounter_test.go b/packages/services/pkg/protocol-parser/hexToPackedCounter_test.go index 21065a7f83..0bc6e60149 100644 --- a/packages/services/pkg/protocol-parser/hexToPackedCounter_test.go +++ b/packages/services/pkg/protocol-parser/hexToPackedCounter_test.go @@ -7,7 +7,7 @@ import ( ) func TestHexToPackedCounter(t *testing.T) { - hex := "0x0000000000008000000000200000000020000000004000000000000000000000" + hex := "0x0000000000000000000000000000400000000020000000002000000000000080" expectedCounter := protocolparser.PackedCounter{ TotalByteLength: uint64(128), FieldByteLengths: []uint64{ @@ -47,7 +47,7 @@ func TestHexToPackedCounterLengthMismatch(t *testing.T) { } }() - hex := "0x0000000000004000000000200000000020000000004000000000000000000000" + hex := "0x0000000000000000000000000000400000000020000000002000000000000040" protocolparser.HexToPackedCounter(hex) t.Error("expected panic") } diff --git a/packages/store-sync/src/blockLogsToStorage.test.ts b/packages/store-sync/src/blockLogsToStorage.test.ts index 843d76aea4..f40d569831 100644 --- a/packages/store-sync/src/blockLogsToStorage.test.ts +++ b/packages/store-sync/src/blockLogsToStorage.test.ts @@ -73,7 +73,7 @@ describe("blockLogsToStorage", () => { { address: "0x5fbdb2315678afecb367f032d93f642f64180aa3", topics: ["0x912af873e852235aae78a1d25ae9bb28b616a67c36898c53a14fd8184504ee32"], - data: "0x6d756473746f7265000000000000000053746f72654d65746164617461000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000496e76656e746f72790000000000000000000000000000000000000000000000000000000000000000000000000000c9000000000000a9000000000900000000a0000000000000000000000000000000496e76656e746f7279000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000576616c75650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + data: "0x6d756473746f7265000000000000000053746f72654d65746164617461000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000496e76656e746f72790000000000000000000000000000000000000000000000000000000000000000000000000000c900000000000000000000000000000000000000a00000000009000000000000a9496e76656e746f7279000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000576616c75650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", blockNumber: 5448n, transactionHash: "0x80d6650fdd6656461e6639988d7baa8d6d228297df505d8bbd0a4efc273b382b", transactionIndex: 44, @@ -83,7 +83,7 @@ describe("blockLogsToStorage", () => { args: { table: "0x6d756473746f7265000000000000000053746f72654d65746164617461000000", key: ["0x00000000000000000000000000000000496e76656e746f727900000000000000"], - data: "0x000000000000a9000000000900000000a0000000000000000000000000000000496e76656e746f7279000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000", + data: "0x00000000000000000000000000000000000000a00000000009000000000000a9496e76656e746f7279000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000576616c7565000000000000000000000000000000000000000000000000000000", }, eventName: "StoreSetRecord", }, diff --git a/packages/store-sync/src/sqlite/sqliteStorage.test.ts b/packages/store-sync/src/sqlite/sqliteStorage.test.ts index 4035fc9833..e21cd954e9 100644 --- a/packages/store-sync/src/sqlite/sqliteStorage.test.ts +++ b/packages/store-sync/src/sqlite/sqliteStorage.test.ts @@ -62,7 +62,7 @@ describe("sqliteStorage", async () => { { address: "0x5fbdb2315678afecb367f032d93f642f64180aa3", topics: ["0x912af873e852235aae78a1d25ae9bb28b616a67c36898c53a14fd8184504ee32"], - data: "0x6d756473746f726500000000000000005461626c657300000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000496e76656e746f7279000000000000000000000000000000000000000000000000000000000000000000000000000260001c030061030300000000000000000000000000000000000000000000000000000401000300000000000000000000000000000000000000000000000000000000000000000200000000016000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000056f776e657200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046974656d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b6974656d56617269616e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006616d6f756e740000000000000000000000000000000000000000000000000000", + data: "0x6d756473746f726500000000000000005461626c657300000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000496e76656e746f7279000000000000000000000000000000000000000000000000000000000000000000000000000260001c030061030300000000000000000000000000000000000000000000000000000401000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000001600000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000056f776e657200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046974656d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b6974656d56617269616e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006616d6f756e740000000000000000000000000000000000000000000000000000", blockHash: "0x4ad3752c86f900332e0d2d8903480e7206747d233586574d16f006eebdb5138b", blockNumber: 2n, transactionHash: "0xaa54bf18053cce5d4d2906538a60cb1d9958cc3c10c34b5f9fdc92fe6a6abab4", @@ -72,7 +72,7 @@ describe("sqliteStorage", async () => { args: { table: "0x6d756473746f726500000000000000005461626c657300000000000000000000", key: ["0x00000000000000000000000000000000496e76656e746f727900000000000000"], - data: "0x001c030061030300000000000000000000000000000000000000000000000000000401000300000000000000000000000000000000000000000000000000000000000000000200000000016000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000056f776e657200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046974656d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b6974656d56617269616e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006616d6f756e740000000000000000000000000000000000000000000000000000", + data: "0x001c030061030300000000000000000000000000000000000000000000000000000401000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000001600000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000056f776e657200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000046974656d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b6974656d56617269616e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000006616d6f756e740000000000000000000000000000000000000000000000000000", }, eventName: "StoreSetRecord", }, diff --git a/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json b/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json index 494ec13752..c71b2f21d8 100644 --- a/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json +++ b/packages/store/abi/MirrorSubscriber.sol/MirrorSubscriber.abi.json @@ -30,6 +30,17 @@ "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/store/abi/PackedCounter.sol/PackedCounterInstance.abi.json b/packages/store/abi/PackedCounter.sol/PackedCounterInstance.abi.json new file mode 100644 index 0000000000..377085b5aa --- /dev/null +++ b/packages/store/abi/PackedCounter.sol/PackedCounterInstance.abi.json @@ -0,0 +1,13 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/store/abi/StoreMock.sol/StoreMock.abi.json b/packages/store/abi/StoreMock.sol/StoreMock.abi.json index 34cbc541dc..cb871d1238 100644 --- a/packages/store/abi/StoreMock.sol/StoreMock.abi.json +++ b/packages/store/abi/StoreMock.sol/StoreMock.abi.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/store/gas-report.json b/packages/store/gas-report.json index 3bfaafc4e6..eb66f1e40b 100644 --- a/packages/store/gas-report.json +++ b/packages/store/gas-report.json @@ -75,25 +75,25 @@ "file": "test/Gas.t.sol", "test": "testCompareAbiEncodeVsCustom", "name": "custom encode", - "gasUsed": 2806 + "gasUsed": 1394 }, { "file": "test/Gas.t.sol", "test": "testCompareAbiEncodeVsCustom", "name": "custom decode", - "gasUsed": 2571 + "gasUsed": 1976 }, { "file": "test/Gas.t.sol", "test": "testCompareAbiEncodeVsCustom", "name": "pass abi encoded bytes to external contract", - "gasUsed": 6551 + "gasUsed": 6550 }, { "file": "test/Gas.t.sol", "test": "testCompareAbiEncodeVsCustom", "name": "pass custom encoded bytes to external contract", - "gasUsed": 1445 + "gasUsed": 1444 }, { "file": "test/Gas.t.sol", @@ -243,7 +243,7 @@ "file": "test/KeyEncoding.t.sol", "test": "testRegisterAndGetSchema", "name": "register KeyEncoding schema", - "gasUsed": 671539 + "gasUsed": 669538 }, { "file": "test/Mixed.t.sol", @@ -255,43 +255,49 @@ "file": "test/Mixed.t.sol", "test": "testRegisterAndGetSchema", "name": "register Mixed schema", - "gasUsed": 533230 + "gasUsed": 531229 }, { "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "set record in Mixed", - "gasUsed": 109280 + "gasUsed": 107190 }, { "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "get record from Mixed", - "gasUsed": 10944 + "gasUsed": 9985 }, { "file": "test/PackedCounter.t.sol", "test": "testAtIndex", "name": "get value at index of PackedCounter", - "gasUsed": 255 + "gasUsed": 24 }, { "file": "test/PackedCounter.t.sol", - "test": "testSetAtIndex", - "name": "set value at index of PackedCounter", - "gasUsed": 793 + "test": "testGas", + "name": "pack 1 length into PackedCounter", + "gasUsed": 35 }, { "file": "test/PackedCounter.t.sol", - "test": "testTotal", - "name": "pack uint40 array into PackedCounter", - "gasUsed": 2146 + "test": "testGas", + "name": "pack 4 lengths into PackedCounter", + "gasUsed": 169 }, { "file": "test/PackedCounter.t.sol", - "test": "testTotal", + "test": "testGas", "name": "get total of PackedCounter", - "gasUsed": 27 + "gasUsed": 15 + }, + { + "file": "test/PackedCounter.t.sol", + "test": "testSetAtIndex", + "name": "set value at index of PackedCounter", + "gasUsed": 286 }, { "file": "test/Schema.t.sol", @@ -471,73 +477,73 @@ "file": "test/StoreCoreDynamic.t.sol", "test": "testGetSecondFieldLength", "name": "get field length (cold, 1 slot)", - "gasUsed": 7959 + "gasUsed": 7748 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetSecondFieldLength", "name": "get field length (warm, 1 slot)", - "gasUsed": 1956 + "gasUsed": 1745 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetThirdFieldLength", "name": "get field length (warm due to , 2 slots)", - "gasUsed": 7959 + "gasUsed": 7748 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetThirdFieldLength", "name": "get field length (warm, 2 slots)", - "gasUsed": 1956 + "gasUsed": 1745 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromSecondField", "name": "pop from field (cold, 1 slot, 1 uint32 item)", - "gasUsed": 22845 + "gasUsed": 21684 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromSecondField", "name": "pop from field (warm, 1 slot, 1 uint32 item)", - "gasUsed": 16875 + "gasUsed": 15714 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromThirdField", "name": "pop from field (cold, 2 slots, 10 uint32 items)", - "gasUsed": 24594 + "gasUsed": 23433 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromThirdField", "name": "pop from field (warm, 2 slots, 10 uint32 items)", - "gasUsed": 16625 + "gasUsed": 15464 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access non-existing record", - "gasUsed": 6084 + "gasUsed": 6047 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access static field of non-existing record", - "gasUsed": 1507 + "gasUsed": 1506 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access dynamic field of non-existing record", - "gasUsed": 2227 + "gasUsed": 2015 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access length of dynamic field of non-existing record", - "gasUsed": 1329 + "gasUsed": 1118 }, { "file": "test/StoreCoreGas.t.sol", @@ -549,7 +555,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testDeleteData", "name": "delete record (complex data, 3 slots)", - "gasUsed": 8613 + "gasUsed": 8400 }, { "file": "test/StoreCoreGas.t.sol", @@ -567,67 +573,67 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "register subscriber", - "gasUsed": 61453 + "gasUsed": 60289 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set record on table with subscriber", - "gasUsed": 70854 + "gasUsed": 70432 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set static field on table with subscriber", - "gasUsed": 26686 + "gasUsed": 26264 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "delete record on table with subscriber", - "gasUsed": 19248 + "gasUsed": 18826 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "register subscriber", - "gasUsed": 61453 + "gasUsed": 60289 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) record on table with subscriber", - "gasUsed": 164158 + "gasUsed": 163278 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) field on table with subscriber", - "gasUsed": 29684 + "gasUsed": 28200 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "delete (dynamic) record on table with subscriber", - "gasUsed": 20855 + "gasUsed": 20432 }, { "file": "test/StoreCoreGas.t.sol", "test": "testPushToField", "name": "push to field (1 slot, 1 uint32 item)", - "gasUsed": 14511 + "gasUsed": 13346 }, { "file": "test/StoreCoreGas.t.sol", "test": "testPushToField", "name": "push to field (2 slots, 10 uint32 items)", - "gasUsed": 37158 + "gasUsed": 35993 }, { "file": "test/StoreCoreGas.t.sol", "test": "testRegisterAndGetSchema", "name": "StoreCore: register schema", - "gasUsed": 596588 + "gasUsed": 594571 }, { "file": "test/StoreCoreGas.t.sol", @@ -645,13 +651,13 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "set complex record with dynamic data (4 slots)", - "gasUsed": 103230 + "gasUsed": 102577 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "get complex record with dynamic data (4 slots)", - "gasUsed": 5095 + "gasUsed": 4619 }, { "file": "test/StoreCoreGas.t.sol", @@ -669,103 +675,103 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicDataLength", "name": "set dynamic length of dynamic index 0", - "gasUsed": 23613 + "gasUsed": 23082 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicDataLength", "name": "set dynamic length of dynamic index 1", - "gasUsed": 1714 + "gasUsed": 1183 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicDataLength", "name": "reduce dynamic length of dynamic index 0", - "gasUsed": 1702 + "gasUsed": 1174 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set static field (1 slot)", - "gasUsed": 33491 + "gasUsed": 33280 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get static field (1 slot)", - "gasUsed": 1508 + "gasUsed": 1507 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set static field (overlap 2 slot)", - "gasUsed": 32368 + "gasUsed": 32157 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get static field (overlap 2 slot)", - "gasUsed": 2264 + "gasUsed": 2265 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set dynamic field (1 slot, first dynamic field)", - "gasUsed": 54942 + "gasUsed": 54199 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get dynamic field (1 slot, first dynamic field)", - "gasUsed": 2404 + "gasUsed": 2193 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set dynamic field (1 slot, second dynamic field)", - "gasUsed": 33060 + "gasUsed": 32317 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get dynamic field (1 slot, second dynamic field)", - "gasUsed": 2412 + "gasUsed": 2201 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticData", "name": "set static record (1 slot)", - "gasUsed": 33017 + "gasUsed": 32806 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticData", "name": "get static record (1 slot)", - "gasUsed": 1263 + "gasUsed": 1247 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticDataSpanningWords", "name": "set static record (2 slots)", - "gasUsed": 55521 + "gasUsed": 55309 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticDataSpanningWords", "name": "get static record (2 slots)", - "gasUsed": 1451 + "gasUsed": 1436 }, { "file": "test/StoreCoreGas.t.sol", "test": "testUpdateInField", "name": "update in field (1 slot, 1 uint32 item)", - "gasUsed": 14046 + "gasUsed": 13623 }, { "file": "test/StoreCoreGas.t.sol", "test": "testUpdateInField", "name": "push to field (2 slots, 6 uint64 items)", - "gasUsed": 15075 + "gasUsed": 14653 }, { "file": "test/StoreSwitch.t.sol", @@ -783,79 +789,79 @@ "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: set field", - "gasUsed": 58949 + "gasUsed": 58202 }, { "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: get field (warm)", - "gasUsed": 4790 + "gasUsed": 4579 }, { "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: push 1 element", - "gasUsed": 38823 + "gasUsed": 37654 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: set field (cold)", - "gasUsed": 60938 + "gasUsed": 60192 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: get field (warm)", - "gasUsed": 4784 + "gasUsed": 4573 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: push 1 element (cold)", - "gasUsed": 38809 + "gasUsed": 37641 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: pop 1 element (warm)", - "gasUsed": 15275 + "gasUsed": 14110 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: push 1 element (warm)", - "gasUsed": 16967 + "gasUsed": 15799 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: update 1 element (warm)", - "gasUsed": 16670 + "gasUsed": 16248 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: delete record (warm)", - "gasUsed": 10008 + "gasUsed": 9797 }, { "file": "test/tables/Hooks.t.sol", "test": "testTable", "name": "Hooks: set field (warm)", - "gasUsed": 33164 + "gasUsed": 32418 }, { "file": "test/tables/HooksColdLoad.t.sol", "test": "testDelete", "name": "Hooks: delete record (cold)", - "gasUsed": 18803 + "gasUsed": 18592 }, { "file": "test/tables/HooksColdLoad.t.sol", "test": "testGet", "name": "Hooks: get field (cold)", - "gasUsed": 10773 + "gasUsed": 10562 }, { "file": "test/tables/HooksColdLoad.t.sol", @@ -867,19 +873,19 @@ "file": "test/tables/HooksColdLoad.t.sol", "test": "testLength", "name": "Hooks: get length (cold)", - "gasUsed": 6991 + "gasUsed": 6780 }, { "file": "test/tables/HooksColdLoad.t.sol", "test": "testPop", "name": "Hooks: pop 1 element (cold)", - "gasUsed": 25396 + "gasUsed": 24231 }, { "file": "test/tables/HooksColdLoad.t.sol", "test": "testUpdate", "name": "Hooks: update 1 element (cold)", - "gasUsed": 26243 + "gasUsed": 25821 }, { "file": "test/tightcoder/DecodeSlice.t.sol", @@ -933,18 +939,18 @@ "file": "test/Vector2.t.sol", "test": "testRegisterAndGetSchema", "name": "register Vector2 schema", - "gasUsed": 394654 + "gasUsed": 392653 }, { "file": "test/Vector2.t.sol", "test": "testSetAndGet", "name": "set Vector2 record", - "gasUsed": 35269 + "gasUsed": 35057 }, { "file": "test/Vector2.t.sol", "test": "testSetAndGet", "name": "get Vector2 record", - "gasUsed": 3620 + "gasUsed": 3604 } ] diff --git a/packages/store/src/PackedCounter.sol b/packages/store/src/PackedCounter.sol index f6fa6a17ac..e8f53ec949 100644 --- a/packages/store/src/PackedCounter.sol +++ b/packages/store/src/PackedCounter.sol @@ -1,98 +1,102 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import { Bytes } from "./Bytes.sol"; - -// - 7 bytes accumulated counter -// - 5 bytes length per counter +// - Last 7 bytes (uint56) are used for the total byte length of the dynamic data +// - The next 5 byte (uint40) sections are used for the byte length of each field, indexed from right to left type PackedCounter is bytes32; -using PackedCounterLib for PackedCounter global; +using PackedCounterInstance for PackedCounter global; -library PackedCounterLib { - /************************************************************************ - * - * STATIC FUNCTIONS - * - ************************************************************************/ +// Number of bits for the 7-byte accumulator +uint256 constant ACC_BITS = 7 * 8; +// Number of bits for the 5-byte sections +uint256 constant VAL_BITS = 5 * 8; +// Maximum value of a 5-byte section +uint256 constant MAX_VAL = type(uint40).max; - /** - * Encode the given counters into a single packed counter - * - 7 bytes for the accumulated length - * - 5 bytes per counter -> max 5 counters - */ - function pack(uint40[] memory counters) internal pure returns (PackedCounter) { - bytes32 packedCounter; - uint56 accumulator; - - // Compute the sum of all counters - // and pack the counters - for (uint256 i; i < counters.length; ) { - packedCounter = Bytes.setBytes5(packedCounter, 7 + 5 * i, bytes5(counters[i])); - accumulator += counters[i]; - unchecked { - i++; - } +/** + * Static functions for PackedCounter + * The caller must ensure that the value arguments are <= MAX_VAL + */ +library PackedCounterLib { + function pack(uint256 a) internal pure returns (PackedCounter) { + uint256 packedCounter; + unchecked { + packedCounter = a; + packedCounter |= (uint256(a) << (ACC_BITS + VAL_BITS * 0)); } - - // Store total length - packedCounter = Bytes.setBytes7(packedCounter, 0, bytes7(accumulator)); - - return PackedCounter.wrap(packedCounter); + return PackedCounter.wrap(bytes32(packedCounter)); } - // Overrides for pack function - function pack(uint40 a) internal pure returns (PackedCounter) { - uint40[] memory counters = new uint40[](1); - counters[0] = a; - return pack(counters); + function pack(uint256 a, uint256 b) internal pure returns (PackedCounter) { + uint256 packedCounter; + unchecked { + packedCounter = a + b; + packedCounter |= (uint256(a) << (ACC_BITS + VAL_BITS * 0)); + packedCounter |= (uint256(b) << (ACC_BITS + VAL_BITS * 1)); + } + return PackedCounter.wrap(bytes32(packedCounter)); } - function pack(uint40 a, uint40 b) internal pure returns (PackedCounter) { - uint40[] memory counters = new uint40[](2); - counters[0] = a; - counters[1] = b; - return pack(counters); + function pack(uint256 a, uint256 b, uint256 c) internal pure returns (PackedCounter) { + uint256 packedCounter; + unchecked { + packedCounter = a + b + c; + packedCounter |= (uint256(a) << (ACC_BITS + VAL_BITS * 0)); + packedCounter |= (uint256(b) << (ACC_BITS + VAL_BITS * 1)); + packedCounter |= (uint256(c) << (ACC_BITS + VAL_BITS * 2)); + } + return PackedCounter.wrap(bytes32(packedCounter)); } - function pack(uint40 a, uint40 b, uint40 c) internal pure returns (PackedCounter) { - uint40[] memory counters = new uint40[](3); - counters[0] = a; - counters[1] = b; - counters[2] = c; - return pack(counters); + function pack(uint256 a, uint256 b, uint256 c, uint256 d) internal pure returns (PackedCounter) { + uint256 packedCounter; + unchecked { + packedCounter = a + b + c + d; + packedCounter |= (uint256(a) << (ACC_BITS + VAL_BITS * 0)); + packedCounter |= (uint256(b) << (ACC_BITS + VAL_BITS * 1)); + packedCounter |= (uint256(c) << (ACC_BITS + VAL_BITS * 2)); + packedCounter |= (uint256(d) << (ACC_BITS + VAL_BITS * 3)); + } + return PackedCounter.wrap(bytes32(packedCounter)); } - function pack(uint40 a, uint40 b, uint40 c, uint40 d) internal pure returns (PackedCounter) { - uint40[] memory counters = new uint40[](4); - counters[0] = a; - counters[1] = b; - counters[2] = c; - counters[3] = d; - return pack(counters); + function pack(uint256 a, uint256 b, uint256 c, uint256 d, uint256 e) internal pure returns (PackedCounter) { + uint256 packedCounter; + unchecked { + packedCounter = a + b + c + d + e; + packedCounter |= (uint256(a) << (ACC_BITS + VAL_BITS * 0)); + packedCounter |= (uint256(b) << (ACC_BITS + VAL_BITS * 1)); + packedCounter |= (uint256(c) << (ACC_BITS + VAL_BITS * 2)); + packedCounter |= (uint256(d) << (ACC_BITS + VAL_BITS * 3)); + packedCounter |= (uint256(e) << (ACC_BITS + VAL_BITS * 4)); + } + return PackedCounter.wrap(bytes32(packedCounter)); } +} - /************************************************************************ - * - * INSTANCE FUNCTIONS - * - ************************************************************************/ +/** + * Instance functions for PackedCounter + */ +library PackedCounterInstance { + error PackedCounter_InvalidLength(uint256 length); /** * Decode the accumulated counter - * (first 7 bytes of packed counter) + * (right-most 7 bytes of packed counter) */ function total(PackedCounter packedCounter) internal pure returns (uint256) { - return uint256(uint56(bytes7(PackedCounter.unwrap(packedCounter)))); + return uint56(uint256(PackedCounter.unwrap(packedCounter))); } /** * Decode the counter at the given index - * (5 bytes per counter after the first 7 bytes) + * (right-to-left, 5 bytes per counter after the right-most 7 bytes) */ - function atIndex(PackedCounter packedCounter, uint256 index) internal pure returns (uint256) { - uint256 offset = 7 + index * 5; - return uint256(uint40(Bytes.slice5(PackedCounter.unwrap(packedCounter), offset))); + function atIndex(PackedCounter packedCounter, uint8 index) internal pure returns (uint256) { + unchecked { + return uint40(uint256(PackedCounter.unwrap(packedCounter) >> (ACC_BITS + VAL_BITS * index))); + } } /** @@ -100,28 +104,44 @@ library PackedCounterLib { */ function setAtIndex( PackedCounter packedCounter, - uint256 index, + uint8 index, uint256 newValueAtIndex ) internal pure returns (PackedCounter) { - bytes32 rawPackedCounter = PackedCounter.unwrap(packedCounter); + if (newValueAtIndex > MAX_VAL) { + revert PackedCounter_InvalidLength(newValueAtIndex); + } + + uint256 rawPackedCounter = uint256(PackedCounter.unwrap(packedCounter)); // Get current lengths (total and at index) uint256 accumulator = total(packedCounter); - uint256 currentValueAtIndex = atIndex(packedCounter, uint8(index)); + uint256 currentValueAtIndex = atIndex(packedCounter, index); // Compute the difference and update the total value - if (newValueAtIndex >= currentValueAtIndex) { - accumulator += newValueAtIndex - currentValueAtIndex; - } else { - accumulator -= currentValueAtIndex - newValueAtIndex; + unchecked { + if (newValueAtIndex >= currentValueAtIndex) { + accumulator += newValueAtIndex - currentValueAtIndex; + } else { + accumulator -= currentValueAtIndex - newValueAtIndex; + } } // Set the new accumulated value and value at index - uint256 offset = 7 + index * 5; // (7 bytes total length, 5 bytes per dynamic schema) - rawPackedCounter = Bytes.setBytes7(rawPackedCounter, 0, bytes7(uint56(accumulator))); - rawPackedCounter = Bytes.setBytes5(rawPackedCounter, offset, bytes5(uint40(newValueAtIndex))); + // (7 bytes total length, 5 bytes per dynamic schema) + uint256 offset; + unchecked { + offset = ACC_BITS + VAL_BITS * index; + } + // Bitmask with 1s at the 5 bytes that form the value slot at the given index + uint256 mask = uint256(type(uint40).max) << offset; + + // First set the last 7 bytes to 0, then set them to the new length + rawPackedCounter = (rawPackedCounter & ~uint256(type(uint56).max)) | accumulator; + + // Zero out the value slot at the given index, then set the new value + rawPackedCounter = (rawPackedCounter & ~mask) | ((newValueAtIndex << offset) & mask); - return PackedCounter.wrap(rawPackedCounter); + return PackedCounter.wrap(bytes32(rawPackedCounter)); } /* diff --git a/packages/store/src/codegen/tables/Callbacks.sol b/packages/store/src/codegen/tables/Callbacks.sol index 9c074695b1..812cab754c 100644 --- a/packages/store/src/codegen/tables/Callbacks.sol +++ b/packages/store/src/codegen/tables/Callbacks.sol @@ -217,9 +217,11 @@ library Callbacks { /** Tightly pack full data using this table's schema */ function encode(bytes24[] memory value) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(value.length * 24); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(value.length * 24); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); } diff --git a/packages/store/src/codegen/tables/Hooks.sol b/packages/store/src/codegen/tables/Hooks.sol index 018eb88dd6..ba3545cdbc 100644 --- a/packages/store/src/codegen/tables/Hooks.sol +++ b/packages/store/src/codegen/tables/Hooks.sol @@ -217,9 +217,11 @@ library Hooks { /** Tightly pack full data using this table's schema */ function encode(address[] memory value) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(value.length * 20); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(value.length * 20); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); } diff --git a/packages/store/src/codegen/tables/Mixed.sol b/packages/store/src/codegen/tables/Mixed.sol index acc2f23fae..2f80eb45e2 100644 --- a/packages/store/src/codegen/tables/Mixed.sol +++ b/packages/store/src/codegen/tables/Mixed.sol @@ -518,10 +518,11 @@ library Mixed { /** Tightly pack full data using this table's schema */ function encode(uint32 u32, uint128 u128, uint32[] memory a32, string memory s) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](2); - _counters[0] = uint40(a32.length * 4); - _counters[1] = uint40(bytes(s).length); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(a32.length * 4, bytes(s).length); + } return abi.encodePacked(u32, u128, _encodedLengths.unwrap(), EncodeArray.encode((a32)), bytes((s))); } diff --git a/packages/store/src/codegen/tables/Tables.sol b/packages/store/src/codegen/tables/Tables.sol index fca77f4ba8..b032e66434 100644 --- a/packages/store/src/codegen/tables/Tables.sol +++ b/packages/store/src/codegen/tables/Tables.sol @@ -550,10 +550,11 @@ library Tables { bytes memory abiEncodedKeyNames, bytes memory abiEncodedFieldNames ) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](2); - _counters[0] = uint40(bytes(abiEncodedKeyNames).length); - _counters[1] = uint40(bytes(abiEncodedFieldNames).length); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(bytes(abiEncodedKeyNames).length, bytes(abiEncodedFieldNames).length); + } return abi.encodePacked( diff --git a/packages/store/test/Mixed.t.sol b/packages/store/test/Mixed.t.sol index 6e209a17fc..1170c14a0c 100644 --- a/packages/store/test/Mixed.t.sol +++ b/packages/store/test/Mixed.t.sol @@ -65,7 +65,7 @@ contract MixedTest is Test, GasReporter, StoreReadWithStubs { assertEq( Mixed.encode(1, 2, a32, s), - hex"0000000100000000000000000000000000000002000000000000130000000008000000000b0000000000000000000000000000000000000300000004736f6d6520737472696e67" + hex"0000000100000000000000000000000000000002000000000000000000000000000000000000000b0000000008000000000000130000000300000004736f6d6520737472696e67" ); } } diff --git a/packages/store/test/PackedCounter.t.sol b/packages/store/test/PackedCounter.t.sol index 4ad489a756..5da1cfcf2e 100644 --- a/packages/store/test/PackedCounter.t.sol +++ b/packages/store/test/PackedCounter.t.sol @@ -6,35 +6,56 @@ import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; import { PackedCounter, PackedCounterLib } from "../src/PackedCounter.sol"; contract PackedCounterTest is Test, GasReporter { - function testTotal() public { - uint40[] memory counters = new uint40[](4); - counters[0] = 1; - counters[1] = 2; - counters[2] = 3; - counters[3] = 4; - - startGasReport("pack uint40 array into PackedCounter"); - PackedCounter packedCounter = PackedCounterLib.pack(counters); - endGasReport(); + function testPack1() public { + PackedCounter packedCounter = PackedCounterLib.pack(4); + assertEq(packedCounter.atIndex(0), 4); - startGasReport("get total of PackedCounter"); - packedCounter.total(); - endGasReport(); + assertEq(packedCounter.total(), 4); + } + + function testPack2() public { + PackedCounter packedCounter = PackedCounterLib.pack(4, 8); + assertEq(packedCounter.atIndex(0), 4); + assertEq(packedCounter.atIndex(1), 8); + + assertEq(packedCounter.total(), 12); + } + + function testPack3() public { + PackedCounter packedCounter = PackedCounterLib.pack(4, 8, 16); + assertEq(packedCounter.atIndex(0), 4); + assertEq(packedCounter.atIndex(1), 8); + assertEq(packedCounter.atIndex(2), 16); + + assertEq(packedCounter.total(), 28); + } + + function testPack4() public { + PackedCounter packedCounter = PackedCounterLib.pack(4, 8, 16, 32); + assertEq(packedCounter.atIndex(0), 4); + assertEq(packedCounter.atIndex(1), 8); + assertEq(packedCounter.atIndex(2), 16); + assertEq(packedCounter.atIndex(3), 32); - assertEq(packedCounter.total(), 10); + assertEq(packedCounter.total(), 60); } - function testAtIndex() public { - uint40[] memory counters = new uint40[](4); - counters[0] = 1; - counters[1] = 2; - counters[2] = 3; - counters[3] = 4; + function testPack5() public { + PackedCounter packedCounter = PackedCounterLib.pack(4, 8, 16, 32, 64); + assertEq(packedCounter.atIndex(0), 4); + assertEq(packedCounter.atIndex(1), 8); + assertEq(packedCounter.atIndex(2), 16); + assertEq(packedCounter.atIndex(3), 32); + assertEq(packedCounter.atIndex(4), 64); - PackedCounter packedCounter = PackedCounterLib.pack(counters); + assertEq(packedCounter.total(), 124); + } + + function testAtIndex() public returns (uint256 value) { + PackedCounter packedCounter = PackedCounterLib.pack(1, 2, 3, 4); startGasReport("get value at index of PackedCounter"); - packedCounter.atIndex(3); + value = packedCounter.atIndex(3); endGasReport(); assertEq(packedCounter.atIndex(0), 1); @@ -44,13 +65,7 @@ contract PackedCounterTest is Test, GasReporter { } function testSetAtIndex() public { - uint40[] memory counters = new uint40[](4); - counters[0] = 1; - counters[1] = 2; - counters[2] = 3; - counters[3] = 4; - - PackedCounter packedCounter = PackedCounterLib.pack(counters); + PackedCounter packedCounter = PackedCounterLib.pack(1, 2, 3, 4); startGasReport("set value at index of PackedCounter"); packedCounter = packedCounter.setAtIndex(2, 5); @@ -62,4 +77,29 @@ contract PackedCounterTest is Test, GasReporter { assertEq(packedCounter.atIndex(3), 4); assertEq(packedCounter.total(), 12); } + + function testGas() public returns (PackedCounter packedCounter, uint256 total) { + bytes32[] memory a1 = new bytes32[](1); + address[] memory a2 = new address[](2); + uint8[] memory a3 = new uint8[](3); + int128[] memory a4 = new int128[](4); + + startGasReport("pack 1 length into PackedCounter"); + unchecked { + packedCounter = PackedCounterLib.pack(a1.length * 32); + } + endGasReport(); + assertEq(packedCounter.total(), 1 * 32); + + startGasReport("pack 4 lengths into PackedCounter"); + unchecked { + packedCounter = PackedCounterLib.pack(a1.length * 32, a2.length * 20, a3.length * 1, a4.length * 16); + } + endGasReport(); + + startGasReport("get total of PackedCounter"); + total = packedCounter.total(); + endGasReport(); + assertEq(packedCounter.total(), 1 * 32 + 2 * 20 + 3 * 1 + 4 * 16); + } } diff --git a/packages/store/test/StoreCore.t.sol b/packages/store/test/StoreCore.t.sol index 5e66762dc8..23fd259c5d 100644 --- a/packages/store/test/StoreCore.t.sol +++ b/packages/store/test/StoreCore.t.sol @@ -290,10 +290,7 @@ contract StoreCoreTest is Test, StoreMock { PackedCounter encodedDynamicLength; { - uint40[] memory dynamicLengths = new uint40[](2); - dynamicLengths[0] = uint40(secondDataBytes.length); - dynamicLengths[1] = uint40(thirdDataBytes.length); - encodedDynamicLength = PackedCounterLib.pack(dynamicLengths); + encodedDynamicLength = PackedCounterLib.pack(uint40(secondDataBytes.length), uint40(thirdDataBytes.length)); } // Concat data @@ -505,10 +502,7 @@ contract StoreCoreTest is Test, StoreMock { PackedCounter encodedDynamicLength; { - uint40[] memory dynamicLengths = new uint40[](2); - dynamicLengths[0] = uint40(secondDataBytes.length); - dynamicLengths[1] = uint40(thirdDataBytes.length); - encodedDynamicLength = PackedCounterLib.pack(dynamicLengths); + encodedDynamicLength = PackedCounterLib.pack(uint40(secondDataBytes.length), uint40(thirdDataBytes.length)); } // Concat data diff --git a/packages/store/test/StoreCoreGas.t.sol b/packages/store/test/StoreCoreGas.t.sol index 4ba03ac530..9c0f7fa722 100644 --- a/packages/store/test/StoreCoreGas.t.sol +++ b/packages/store/test/StoreCoreGas.t.sol @@ -196,13 +196,10 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { thirdDataBytes = EncodeArray.encode(thirdData); } - PackedCounter encodedDynamicLength; - { - uint40[] memory dynamicLengths = new uint40[](2); - dynamicLengths[0] = uint40(secondDataBytes.length); - dynamicLengths[1] = uint40(thirdDataBytes.length); - encodedDynamicLength = PackedCounterLib.pack(dynamicLengths); - } + PackedCounter encodedDynamicLength = PackedCounterLib.pack( + uint40(secondDataBytes.length), + uint40(thirdDataBytes.length) + ); // Concat data bytes memory data = abi.encodePacked( @@ -363,13 +360,10 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { thirdDataBytes = EncodeArray.encode(thirdData); } - PackedCounter encodedDynamicLength; - { - uint40[] memory dynamicLengths = new uint40[](2); - dynamicLengths[0] = uint40(secondDataBytes.length); - dynamicLengths[1] = uint40(thirdDataBytes.length); - encodedDynamicLength = PackedCounterLib.pack(dynamicLengths); - } + PackedCounter encodedDynamicLength = PackedCounterLib.pack( + uint40(secondDataBytes.length), + uint40(thirdDataBytes.length) + ); // Concat data bytes memory data = abi.encodePacked( diff --git a/packages/store/ts/codegen/renderTable.ts b/packages/store/ts/codegen/renderTable.ts index 7549a52ad6..a4df23466a 100644 --- a/packages/store/ts/codegen/renderTable.ts +++ b/packages/store/ts/codegen/renderTable.ts @@ -160,15 +160,21 @@ ${renderTypeHelpers(options)} function renderEncodedLengths(dynamicFields: RenderDynamicField[]) { if (dynamicFields.length > 0) { return ` - uint40[] memory _counters = new uint40[](${dynamicFields.length}); - ${renderList(dynamicFields, ({ name, arrayElement }, index) => { - if (arrayElement) { - return `_counters[${index}] = uint40(${name}.length * ${arrayElement.staticByteLength});`; - } else { - return `_counters[${index}] = uint40(bytes(${name}).length);`; - } - })} - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack( + ${renderArguments( + dynamicFields.map(({ name, arrayElement }) => { + if (arrayElement) { + return `${name}.length * ${arrayElement.staticByteLength}`; + } else { + return `bytes(${name}).length`; + } + }) + )} + ); + } `; } else { return ""; diff --git a/packages/world/abi/AccessManagementSystem.sol/AccessManagementSystem.abi.json b/packages/world/abi/AccessManagementSystem.sol/AccessManagementSystem.abi.json index 5fe389fa1a..e9c1bdf054 100644 --- a/packages/world/abi/AccessManagementSystem.sol/AccessManagementSystem.abi.json +++ b/packages/world/abi/AccessManagementSystem.sol/AccessManagementSystem.abi.json @@ -15,6 +15,17 @@ "name": "AccessDenied", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/CoreModule.sol/CoreModule.abi.json b/packages/world/abi/CoreModule.sol/CoreModule.abi.json index 7aca0b168b..43437a8fc5 100644 --- a/packages/world/abi/CoreModule.sol/CoreModule.abi.json +++ b/packages/world/abi/CoreModule.sol/CoreModule.abi.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json index d72a6654c0..fa1cac5fff 100644 --- a/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json +++ b/packages/world/abi/CoreSystem.sol/CoreSystem.abi.json @@ -59,6 +59,17 @@ "name": "ModuleAlreadyInstalled", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json b/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json index 223ef6484f..3b28134403 100644 --- a/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json +++ b/packages/world/abi/KeysInTableHook.sol/KeysInTableHook.abi.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json b/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json index a4a456e299..cba5bb5b68 100644 --- a/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json +++ b/packages/world/abi/KeysWithValueHook.sol/KeysWithValueHook.abi.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json b/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json index b23c956332..aeaee69069 100644 --- a/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json +++ b/packages/world/abi/KeysWithValueModule.sol/KeysWithValueModule.abi.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/PackedCounter.sol/PackedCounterInstance.abi.json b/packages/world/abi/PackedCounter.sol/PackedCounterInstance.abi.json new file mode 100644 index 0000000000..377085b5aa --- /dev/null +++ b/packages/world/abi/PackedCounter.sol/PackedCounterInstance.abi.json @@ -0,0 +1,13 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json index 7b5c1500ce..1132390e96 100644 --- a/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json +++ b/packages/world/abi/StoreRegistrationSystem.sol/StoreRegistrationSystem.abi.json @@ -59,6 +59,17 @@ "name": "ModuleAlreadyInstalled", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/UniqueEntitySystem.sol/UniqueEntitySystem.abi.json b/packages/world/abi/UniqueEntitySystem.sol/UniqueEntitySystem.abi.json index e4f117c782..ea11319630 100644 --- a/packages/world/abi/UniqueEntitySystem.sol/UniqueEntitySystem.abi.json +++ b/packages/world/abi/UniqueEntitySystem.sol/UniqueEntitySystem.abi.json @@ -1,4 +1,15 @@ [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/World.sol/World.abi.json b/packages/world/abi/World.sol/World.abi.json index e146910aad..852942f8fc 100644 --- a/packages/world/abi/World.sol/World.abi.json +++ b/packages/world/abi/World.sol/World.abi.json @@ -64,6 +64,17 @@ "name": "ModuleAlreadyInstalled", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json index fefed7e161..6d19d7475e 100644 --- a/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json +++ b/packages/world/abi/WorldRegistrationSystem.sol/WorldRegistrationSystem.abi.json @@ -59,6 +59,17 @@ "name": "ModuleAlreadyInstalled", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + }, { "inputs": [ { diff --git a/packages/world/abi/src/PackedCounter.sol/PackedCounterInstance.abi.json b/packages/world/abi/src/PackedCounter.sol/PackedCounterInstance.abi.json new file mode 100644 index 0000000000..377085b5aa --- /dev/null +++ b/packages/world/abi/src/PackedCounter.sol/PackedCounterInstance.abi.json @@ -0,0 +1,13 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "PackedCounter_InvalidLength", + "type": "error" + } +] \ No newline at end of file diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index a86fe2413a..dff4daad5e 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -3,73 +3,73 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1418450 + "gasUsed": 1411022 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1418450 + "gasUsed": 1411022 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 183830 + "gasUsed": 182044 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1418450 + "gasUsed": 1411022 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1418450 + "gasUsed": 1411022 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 25867 + "gasUsed": 25656 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 256284 + "gasUsed": 250709 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1418450 + "gasUsed": 1411022 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 24587 + "gasUsed": 24376 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 131341 + "gasUsed": 128910 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 654924 + "gasUsed": 650020 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "Get list of keys with a given value", - "gasUsed": 7120 + "gasUsed": 6909 }, { "file": "test/KeysWithValueModule.t.sol", @@ -81,222 +81,222 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 654924 + "gasUsed": 650020 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 153345 + "gasUsed": 151544 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 654924 + "gasUsed": 650020 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 120334 + "gasUsed": 118016 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "delete a record on a table with KeysWithValueModule installed", - "gasUsed": 44242 + "gasUsed": 43594 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 654924 + "gasUsed": 650020 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "set a field on a table with KeysWithValueModule installed", - "gasUsed": 160304 + "gasUsed": 158488 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "change a field on a table with KeysWithValueModule installed", - "gasUsed": 122562 + "gasUsed": 120746 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueNotQuery", "name": "CombinedHasHasValueNotQuery", - "gasUsed": 166965 + "gasUsed": 165699 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueQuery", "name": "CombinedHasHasValueQuery", - "gasUsed": 76840 + "gasUsed": 75996 }, { "file": "test/query.t.sol", "test": "testCombinedHasNotQuery", "name": "CombinedHasNotQuery", - "gasUsed": 230910 + "gasUsed": 229855 }, { "file": "test/query.t.sol", "test": "testCombinedHasQuery", "name": "CombinedHasQuery", - "gasUsed": 152432 + "gasUsed": 151588 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueNotQuery", "name": "CombinedHasValueNotQuery", - "gasUsed": 144314 + "gasUsed": 143470 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueQuery", "name": "CombinedHasValueQuery", - "gasUsed": 18371 + "gasUsed": 17949 }, { "file": "test/query.t.sol", "test": "testHasQuery", "name": "HasQuery", - "gasUsed": 35094 + "gasUsed": 34883 }, { "file": "test/query.t.sol", "test": "testHasQuery1000Keys", "name": "HasQuery with 1000 keys", - "gasUsed": 9272856 + "gasUsed": 9272645 }, { "file": "test/query.t.sol", "test": "testHasQuery100Keys", "name": "HasQuery with 100 keys", - "gasUsed": 861589 + "gasUsed": 861378 }, { "file": "test/query.t.sol", "test": "testHasValueQuery", "name": "HasValueQuery", - "gasUsed": 8862 + "gasUsed": 8651 }, { "file": "test/query.t.sol", "test": "testNotValueQuery", "name": "NotValueQuery", - "gasUsed": 70434 + "gasUsed": 69590 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 726356 + "gasUsed": 721265 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "get a unique entity nonce (non-root module)", - "gasUsed": 65475 + "gasUsed": 65023 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 705186 + "gasUsed": 700336 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "get a unique entity nonce (root module)", - "gasUsed": 65475 + "gasUsed": 65023 }, { "file": "test/World.t.sol", "test": "testDeleteRecord", "name": "Delete record", - "gasUsed": 12412 + "gasUsed": 12201 }, { "file": "test/World.t.sol", "test": "testPushToField", "name": "Push data to the table", - "gasUsed": 92561 + "gasUsed": 91408 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 70459 + "gasUsed": 70007 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 63952 + "gasUsed": 63500 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 91053 + "gasUsed": 90601 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 140713 + "gasUsed": 139839 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 79863 + "gasUsed": 79411 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 653043 + "gasUsed": 649941 }, { "file": "test/World.t.sol", "test": "testSetField", "name": "Write data to a table field", - "gasUsed": 40944 + "gasUsed": 40733 }, { "file": "test/World.t.sol", "test": "testSetRecord", "name": "Write data to the table", - "gasUsed": 39807 + "gasUsed": 39596 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (cold)", - "gasUsed": 32258 + "gasUsed": 31108 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (warm)", - "gasUsed": 19048 + "gasUsed": 17898 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (cold)", - "gasUsed": 33942 + "gasUsed": 33520 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (warm)", - "gasUsed": 21146 + "gasUsed": 20724 } ] diff --git a/packages/world/src/modules/core/tables/SystemHooks.sol b/packages/world/src/modules/core/tables/SystemHooks.sol index 8ee3d6aeee..0489ccbb7a 100644 --- a/packages/world/src/modules/core/tables/SystemHooks.sol +++ b/packages/world/src/modules/core/tables/SystemHooks.sol @@ -217,9 +217,11 @@ library SystemHooks { /** Tightly pack full data using this table's schema */ function encode(address[] memory value) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(value.length * 20); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(value.length * 20); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); } diff --git a/packages/world/src/modules/keysintable/tables/KeysInTable.sol b/packages/world/src/modules/keysintable/tables/KeysInTable.sol index a81be52b10..c0a6832e7e 100644 --- a/packages/world/src/modules/keysintable/tables/KeysInTable.sol +++ b/packages/world/src/modules/keysintable/tables/KeysInTable.sol @@ -970,13 +970,17 @@ library KeysInTable { bytes32[] memory keys3, bytes32[] memory keys4 ) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](5); - _counters[0] = uint40(keys0.length * 32); - _counters[1] = uint40(keys1.length * 32); - _counters[2] = uint40(keys2.length * 32); - _counters[3] = uint40(keys3.length * 32); - _counters[4] = uint40(keys4.length * 32); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack( + keys0.length * 32, + keys1.length * 32, + keys2.length * 32, + keys3.length * 32, + keys4.length * 32 + ); + } return abi.encodePacked( diff --git a/packages/world/src/modules/keyswithvalue/tables/KeysWithValue.sol b/packages/world/src/modules/keyswithvalue/tables/KeysWithValue.sol index a37a92117e..1f85d91d59 100644 --- a/packages/world/src/modules/keyswithvalue/tables/KeysWithValue.sol +++ b/packages/world/src/modules/keyswithvalue/tables/KeysWithValue.sol @@ -218,9 +218,11 @@ library KeysWithValue { /** Tightly pack full data using this table's schema */ function encode(bytes32[] memory keysWithValue) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(keysWithValue.length * 32); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(keysWithValue.length * 32); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((keysWithValue))); } diff --git a/packages/world/test/tables/AddressArray.sol b/packages/world/test/tables/AddressArray.sol index d7872f3825..e17350eaac 100644 --- a/packages/world/test/tables/AddressArray.sol +++ b/packages/world/test/tables/AddressArray.sol @@ -214,9 +214,11 @@ library AddressArray { /** Tightly pack full data using this table's schema */ function encode(address[] memory value) internal pure returns (bytes memory) { - uint40[] memory _counters = new uint40[](1); - _counters[0] = uint40(value.length * 20); - PackedCounter _encodedLengths = PackedCounterLib.pack(_counters); + PackedCounter _encodedLengths; + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = PackedCounterLib.pack(value.length * 20); + } return abi.encodePacked(_encodedLengths.unwrap(), EncodeArray.encode((value))); }