diff --git a/.gitmodules b/.gitmodules index 72e1a2a..0f613c2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/p256-verifier"] path = lib/p256-verifier url = https://github.com/daimo-eth/p256-verifier +[submodule "lib/inflate-sol"] + path = lib/inflate-sol + url = https://github.com/adlerjohn/inflate-sol diff --git a/lib/inflate-sol b/lib/inflate-sol new file mode 160000 index 0000000..2a88141 --- /dev/null +++ b/lib/inflate-sol @@ -0,0 +1 @@ +Subproject commit 2a88141f5226da9d0252be4a456a2e0b23ba3d0e diff --git a/package-lock.json b/package-lock.json index caceb6a..a3c1090 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@daimo/bulk", - "version": "0.1.0", + "version": "0.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@daimo/bulk", - "version": "0.1.0", + "version": "0.2.4", "license": "GPL-3.0-or-later", "dependencies": { "@tsconfig/node20": "^20.1.2", "@wagmi/cli": "^1.5.2", - "typescript": "^5.3.3" + "pako": "^2.1.0", + "typescript": "^5.0.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -1521,6 +1522,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", diff --git a/package.json b/package.json index 53b79b5..c78579f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "test": "forge test -vv", + "test": "forge test -vv --ffi", "build": "tsc", "codegen": "wagmi generate" }, @@ -16,6 +16,7 @@ "dependencies": { "@tsconfig/node20": "^20.1.2", "@wagmi/cli": "^1.5.2", + "pako": "^2.1.0", "typescript": "^5.0.0" }, "publishConfig": { diff --git a/remappings.txt b/remappings.txt index b8512d0..86a0301 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,4 +2,5 @@ forge-std/=lib/forge-std/src/ account-abstraction/=lib/account-abstraction/contracts/ openzeppelin-contracts/=lib/openzeppelin-contracts/ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -p256-verifier/=lib/p256-verifier/src/ \ No newline at end of file +p256-verifier/=lib/p256-verifier/src/ +inflate-sol/=lib/inflate-sol/contracts/ diff --git a/src/DeflateInflator.sol b/src/DeflateInflator.sol new file mode 100644 index 0000000..12466eb --- /dev/null +++ b/src/DeflateInflator.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.8; + +import "./IInflator.sol"; +import "inflate-sol/InflateLib.sol"; +import "account-abstraction/interfaces/IEntryPoint.sol"; +import {Test, console2} from "forge-std/Test.sol"; + +/// Inflates a generic bundle compressed with DEFLATE +/// This reduces calldata size by ~72% and calldata cost by ~37% +/// (due to calldata 0-bytes being cheaper than non-0-bytes) +contract DeflateInflator is IInflator { + error InflateLibError(InflateLib.ErrorCode errorCode); + + function inflate( + bytes calldata compressed + ) external view override returns (UserOperation[] memory, address payable) { + (InflateLib.ErrorCode errorCode, bytes memory decompressed) = InflateLib + .puff(compressed[3:], uint24(bytes3(compressed[0:3]))); + + if (errorCode != InflateLib.ErrorCode.ERR_NONE) { + revert InflateLibError(errorCode); + } + + UserOperation[] memory ops = abi.decode( + abi.encodePacked(uint256(0x20), decompressed), + (UserOperation[]) + ); + + return (ops, payable(tx.origin)); + } +} diff --git a/test/BundleBulker.t.sol b/test/BundleBulker.t.sol index 351d708..d535851 100644 --- a/test/BundleBulker.t.sol +++ b/test/BundleBulker.t.sol @@ -2,11 +2,12 @@ pragma solidity ^0.8.13; import {Test, console2} from "forge-std/Test.sol"; -import {UserOperation} from "account-abstraction/interfaces/IEntryPoint.sol"; +import {UserOperation,IEntryPoint} from "account-abstraction/interfaces/IEntryPoint.sol"; import {BundleBulker} from "../src/BundleBulker.sol"; import {IInflator} from "../src/IInflator.sol"; import {DaimoTransferInflator} from "../src/DaimoTransferInflator.sol"; +import {DeflateInflator} from "../src/DeflateInflator.sol"; contract BundleBulkerTest is Test { BundleBulker public b; @@ -152,6 +153,61 @@ contract BundleBulkerTest is Test { ) ); } + + function test_DeflateInflator() public { + DeflateInflator d = new DeflateInflator(); + b.registerInflator(0x42, d); + + // Taken from a real bundle that was submitted to the network: + // https://basescan.org/tx/0xacb32cca8ddefcf73cb16fda17ff34cf15382e4e0cfb7a96e8149011a5fe3d29 + bytes memory raw = hex'000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000008bffa71a959af0b15c6eaa10d244d80bf23cb6a20000000000000000501c58693b65f1374631a2fca7bb7dc600000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000aae6000000000000000000000000000000000000000000000000000000000007b44a300000000000000000000000000000000000000000000000000000000000f427200000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014434fcd5be000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000a1b349c566c44769888948adc061abcdb54497f700000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001499d720cd5a04c16dc5377638e3f6d609c895714f00000000000000000000000000000000000000000000000000000000000000000000000000000000000001e80100006553c75f00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000170000000000000000000000000000000000000000000000000000000000000001ce1a2a89ec9d3cecd1e9fd65808d85702d7f8681d42ce8f0982363a362b87bd5498c72f497f9d27ae895c6d2c10a73e85b73d258371d2322c80ca5bfad242f5f000000000000000000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22415141415a5650485830567a705463726d35665a6846505f566369545433584d57484832624e7a6a6435346531774e354d32696f222c226f726967696e223a226461696d6f2e636f6d227d0000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + UserOperation[] memory originalOps = abi.decode(abi.encodePacked(uint256(0x20), raw), (UserOperation[])); + + string[] memory inputs = new string[](3); + inputs[0] = "node"; + inputs[1] = "test/deflate.js"; + inputs[2] = '000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000008bffa71a959af0b15c6eaa10d244d80bf23cb6a20000000000000000501c58693b65f1374631a2fca7bb7dc600000000000000000000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000493e000000000000000000000000000000000000000000000000000000000000aae6000000000000000000000000000000000000000000000000000000000007b44a300000000000000000000000000000000000000000000000000000000000f427200000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014434fcd5be000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000a1b349c566c44769888948adc061abcdb54497f700000000000000000000000000000000000000000000000000000000000f42400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001499d720cd5a04c16dc5377638e3f6d609c895714f00000000000000000000000000000000000000000000000000000000000000000000000000000000000001e80100006553c75f00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000170000000000000000000000000000000000000000000000000000000000000001ce1a2a89ec9d3cecd1e9fd65808d85702d7f8681d42ce8f0982363a362b87bd5498c72f497f9d27ae895c6d2c10a73e85b73d258371d2322c80ca5bfad242f5f000000000000000000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22415141415a5650485830567a705463726d35665a6846505f566369545433584d57484832624e7a6a6435346531774e354d32696f222c226f726967696e223a226461696d6f2e636f6d227d0000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + bytes memory compressed = vm.ffi(inputs); + + (UserOperation[] memory inflatedOps, address payable beneficiary) = b.inflate( + abi.encodePacked(uint32(0x42), uint24(raw.length), compressed) + ); + + assertEq(beneficiary, tx.origin); + assertEq(abi.encode(inflatedOps), abi.encode(originalOps)); + + // Calculate and print compression ratios + + bytes memory uncompressedCallData = abi.encodeWithSelector(IEntryPoint.handleOps.selector, originalOps, tx.origin); + uint256 uncompressedCallDataLength = uncompressedCallData.length; + uint256 uncompressedCallDataCost = calculateCalldataCost(uncompressedCallData); + + bytes memory compressedCallData = abi.encodePacked(uint32(0x42), uint24(raw.length), compressed); + uint256 compressedCallDataLength = compressedCallData.length; + uint256 compressedCallDataCost = calculateCalldataCost(compressedCallData); + + console2.log("Uncompressed calldata length: ", uncompressedCallDataLength); + console2.log("Deflate compressed calldata length: ", compressedCallDataLength); + console2.log("Calldata length compression ratio: ", 100 - (compressedCallDataLength * 1e2) / uncompressedCallDataLength, "%"); + console2.log(""); + console2.log("Uncompressed calldata cost: ", uncompressedCallDataCost); + console2.log("Deflate compressed calldata cost: ", compressedCallDataCost); + console2.log("Calldata cost compression ratio: ", 100 - (compressedCallDataCost * 1e2) / uncompressedCallDataCost, "%"); + } + + function calculateCalldataCost(bytes memory callData) public pure returns (uint256 cost) { + // 0 bytes cost 4 gas, non-0 bytes cost 16 gas + for (uint256 i = 0; i < callData.length; i++) { + if (callData[i] == 0) { + cost += 4; + } else { + cost += 16; + } + } + } + + + } contract DummyInflator is IInflator { diff --git a/test/deflate.js b/test/deflate.js new file mode 100644 index 0000000..6aa1086 --- /dev/null +++ b/test/deflate.js @@ -0,0 +1,5 @@ +const pako = require("pako"); + +const compressed = pako.deflateRaw(new Uint8Array(Buffer.from(process.argv[2], 'hex')), { level: 9 }); + +console.log(Buffer.from(compressed).toString('hex'));