-
Notifications
You must be signed in to change notification settings - Fork 12.1k
Compression libraries: FastLZ, LZ4 and Snappy #5820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Amxx
wants to merge
20
commits into
OpenZeppelin:master
Choose a base branch
from
Amxx:feature/compression
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,340
−1
Draft
Changes from 8 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
a206a50
wip
Amxx bf58a07
Update Compression.sol
Amxx 5984e6c
Add snappy uncompress
Amxx c45990d
add FastLZ.decompressCalldata
Amxx 73102f8
doc
Amxx d09ba3c
codespell
Amxx 3641f34
add LiZip helpers
Amxx 4d2eeb3
Apply suggestions from code review
Amxx b341240
Update contracts/utils/compression/FastLZ.sol
Amxx 92a232e
use mcopy
Amxx c5c8553
up
Amxx b06e816
fix tests
Amxx e8139bf
Add LZ4
Amxx ffdc0d2
fix LZ4 + gas comparaison
Amxx 0366797
add stateless
Amxx 775a991
add solady as a dev dependency to use LibZip from solady/js/solady
Amxx 5b39ebf
LZ4: add memory access checks and calldataDecompress
Amxx b35e095
up
Amxx 96745a0
up
Amxx cd20e50
check inputPtr at then end (can only move forward anyway)
Amxx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
/** | ||
* @dev Library for decompressing data using FastLZ. | ||
* | ||
* See https://fr.wikipedia.org/wiki/FastLZ | ||
*/ | ||
library FastLZ { | ||
/** | ||
* @dev FastLZ level 1 decompression. | ||
* | ||
* Based on the reference implementation available here: | ||
* https://github.com/ariya/FastLZ?tab=readme-ov-file#decompressor-reference-implementation | ||
*/ | ||
function decompress(bytes memory input) internal pure returns (bytes memory output) { | ||
assembly ("memory-safe") { | ||
// Use new memory allocate at the FMP | ||
output := mload(0x40) | ||
|
||
// Decrypted inputPtr location | ||
let outputPtr := add(output, 0x20) | ||
|
||
// end of the input inputPtr (input.length after the beginning of the inputPtr) | ||
let end := add(add(input, 0x20), mload(input)) | ||
|
||
for { | ||
let inputPtr := add(input, 0x20) | ||
} lt(inputPtr, end) {} { | ||
let chunk := mload(inputPtr) | ||
let first := byte(0, chunk) | ||
let type_ := shr(5, first) | ||
|
||
switch type_ | ||
case 0 { | ||
mstore(outputPtr, mload(add(inputPtr, 1))) | ||
inputPtr := add(inputPtr, add(2, first)) | ||
outputPtr := add(outputPtr, add(1, first)) | ||
} | ||
case 7 { | ||
let ofs := add(shl(8, and(first, 31)), byte(2, chunk)) | ||
let len := add(9, byte(1, chunk)) | ||
let ref := sub(sub(outputPtr, ofs), 1) | ||
let step := sub(0x20, mul(lt(ofs, 0x20), sub(0x1f, ofs))) // min(ofs+1, 0x20) | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, step) | ||
} { | ||
mstore(add(outputPtr, i), mload(add(ref, i))) | ||
} | ||
inputPtr := add(inputPtr, 3) | ||
outputPtr := add(outputPtr, len) | ||
} | ||
default { | ||
let ofs := add(shl(8, and(first, 31)), byte(1, chunk)) | ||
let len := add(2, type_) | ||
let ref := sub(sub(outputPtr, ofs), 1) | ||
let step := sub(0x20, mul(lt(ofs, 0x20), sub(0x1f, ofs))) // min(ofs+1, 0x20) | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, step) | ||
} { | ||
mstore(add(outputPtr, i), mload(add(ref, i))) | ||
} | ||
inputPtr := add(inputPtr, 2) | ||
outputPtr := add(outputPtr, len) | ||
} | ||
} | ||
mstore(output, sub(outputPtr, add(output, 0x20))) | ||
mstore(0x40, outputPtr) | ||
} | ||
} | ||
|
||
function decompressCalldata(bytes calldata input) internal pure returns (bytes memory output) { | ||
assembly ("memory-safe") { | ||
// Use new memory allocate at the FMP | ||
output := mload(0x40) | ||
|
||
// Decrypted inputPtr location | ||
let outputPtr := add(output, 0x20) | ||
|
||
// end of the input inputPtr (input.length after the beginning of the inputPtr) | ||
let end := add(input.offset, input.length) | ||
|
||
for { | ||
let inputPtr := input.offset | ||
} lt(inputPtr, end) {} { | ||
let chunk := calldataload(inputPtr) | ||
let first := byte(0, chunk) | ||
let type_ := shr(5, first) | ||
|
||
switch type_ | ||
case 0 { | ||
mstore(outputPtr, calldataload(add(inputPtr, 1))) | ||
inputPtr := add(inputPtr, add(2, first)) | ||
outputPtr := add(outputPtr, add(1, first)) | ||
} | ||
case 7 { | ||
let ofs := add(shl(8, and(first, 31)), byte(2, chunk)) | ||
let len := add(9, byte(1, chunk)) | ||
let ref := sub(sub(outputPtr, ofs), 1) | ||
let step := sub(0x20, mul(lt(ofs, 0x20), sub(0x1f, ofs))) // min(ofs+1, 0x20) | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, step) | ||
} { | ||
mstore(add(outputPtr, i), mload(add(ref, i))) | ||
} | ||
inputPtr := add(inputPtr, 3) | ||
outputPtr := add(outputPtr, len) | ||
} | ||
default { | ||
let ofs := add(shl(8, and(first, 31)), byte(1, chunk)) | ||
let len := add(2, type_) | ||
let ref := sub(sub(outputPtr, ofs), 1) | ||
let step := sub(0x20, mul(lt(ofs, 0x20), sub(0x1f, ofs))) // min(ofs+1, 0x20) | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, step) | ||
} { | ||
mstore(add(outputPtr, i), mload(add(ref, i))) | ||
} | ||
inputPtr := add(inputPtr, 2) | ||
outputPtr := add(outputPtr, len) | ||
} | ||
} | ||
mstore(output, sub(outputPtr, add(output, 0x20))) | ||
mstore(0x40, outputPtr) | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.20; | ||
|
||
/** | ||
* @dev Library for decompressing data using Snappy. | ||
* | ||
* See https://github.com/google/snappy | ||
*/ | ||
library Snappy { | ||
error DecodingFailure(); | ||
|
||
/** | ||
* @dev Implementation of Snappy's uncompress function. | ||
* | ||
* Based on https://github.com/zhipeng-jia/snappyjs/blob/v0.7.0/snappy_decompressor.js[snappyjs javascript implementation]. | ||
*/ | ||
function uncompress(bytes memory input) internal pure returns (bytes memory output) { | ||
bytes4 errorSelector = DecodingFailure.selector; | ||
|
||
assembly ("memory-safe") { | ||
// helper: revert with custom error (without args) if boolean isn't true | ||
function assert(b, e) { | ||
if iszero(b) { | ||
mstore(0, e) | ||
revert(0, 0x04) | ||
} | ||
} | ||
|
||
// input buffer bounds | ||
let inputBegin := add(input, 0x20) | ||
let inputEnd := add(inputBegin, mload(input)) | ||
|
||
// input traversal pointer | ||
let inputPtr := inputBegin | ||
// read length of the decompressed buffer | ||
let outputLength := 0 | ||
for {} lt(inputPtr, inputEnd) { | ||
inputPtr := add(inputPtr, 1) | ||
} { | ||
let c := byte(0, mload(inputPtr)) | ||
outputLength := add(outputLength, shl(mul(7, sub(inputPtr, inputBegin)), and(c, 0x7f))) | ||
if iszero(and(c, 0x80)) { | ||
break | ||
} | ||
} | ||
inputPtr := add(inputPtr, 1) | ||
|
||
// allocated output buffer | ||
output := mload(0x40) | ||
let outputPtr := add(output, 0x20) | ||
mstore(output, outputLength) | ||
mstore(0x40, add(outputPtr, outputLength)) | ||
|
||
// decompress input buffer into output buffer | ||
for { | ||
let len, offset | ||
} lt(inputPtr, inputEnd) {} { | ||
// get next (compressed) word -- used as a cache for further reads | ||
let w := mload(inputPtr) | ||
inputPtr := add(inputPtr, 1) | ||
|
||
let c := byte(0, w) | ||
|
||
// consider different cases based on the lower 2 bits of c | ||
// - 0: literal | ||
// - 1,2,3: offset copy | ||
switch and(c, 0x3) | ||
case 0 { | ||
len := add(shr(2, c), 1) | ||
if gt(len, 60) { | ||
assert(lt(add(inputPtr, 3), inputEnd), errorSelector) | ||
let smallLen := sub(len, 60) | ||
len := or(or(byte(1, w), shl(8, byte(2, w))), or(shl(16, byte(3, w)), shl(24, byte(4, w)))) | ||
len := add(and(len, shr(sub(256, mul(8, smallLen)), not(0))), 1) | ||
inputPtr := add(inputPtr, smallLen) | ||
} | ||
assert(not(gt(add(inputPtr, len), inputEnd)), errorSelector) | ||
// copy len bytes from input to output in chunks of 32 bytes | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, 0x20) | ||
} { | ||
mstore(add(outputPtr, i), mload(add(inputPtr, i))) | ||
} | ||
inputPtr := add(inputPtr, len) | ||
outputPtr := add(outputPtr, len) | ||
|
||
// continue to skip the offset copy logic that is shared by the other 3 cases | ||
continue | ||
} | ||
case 1 { | ||
assert(lt(inputPtr, inputEnd), errorSelector) | ||
len := add(and(shr(2, c), 0x7), 4) | ||
offset := add(byte(1, w), shl(8, shr(5, c))) | ||
inputPtr := add(inputPtr, 1) | ||
} | ||
case 2 { | ||
assert(lt(add(inputPtr, 1), inputEnd), errorSelector) | ||
len := add(shr(2, c), 1) | ||
offset := add(byte(1, w), shl(8, byte(2, w))) | ||
inputPtr := add(inputPtr, 2) | ||
} | ||
case 3 { | ||
assert(lt(add(inputPtr, 3), inputEnd), errorSelector) | ||
len := add(shr(2, c), 1) | ||
offset := add(add(byte(1, w), shl(8, byte(2, w))), add(shl(16, byte(3, w)), shl(24, byte(4, w)))) | ||
inputPtr := add(inputPtr, 4) | ||
} | ||
assert(and(iszero(iszero(offset)), not(gt(offset, sub(outputPtr, add(output, 0x20))))), errorSelector) | ||
|
||
// copying in chunks will not work if the offset is larger than the chunk length, so we compute | ||
// `step = Math.min(0x20, offset)` and use it for the memory copy | ||
let step := xor(0x20, mul(lt(offset, 0x20), xor(0x20, offset))) | ||
|
||
// copy len bytes from output to itself. | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, step) | ||
} { | ||
mstore(add(outputPtr, i), mload(sub(add(outputPtr, i), offset))) | ||
} | ||
outputPtr := add(outputPtr, len) | ||
} | ||
|
||
// sanity check, FMP is at the right location | ||
assert(eq(outputPtr, mload(0x40)), errorSelector) | ||
} | ||
} | ||
|
||
/// @dev Variant of {uncompress} that takes a buffer from calldata. | ||
function uncompressCalldata(bytes calldata input) internal pure returns (bytes memory output) { | ||
bytes4 errorSelector = DecodingFailure.selector; | ||
|
||
assembly ("memory-safe") { | ||
// helper: revert with custom error (without args) if boolean isn't true | ||
function assert(b, e) { | ||
if iszero(b) { | ||
mstore(0, e) | ||
revert(0, 0x04) | ||
} | ||
} | ||
|
||
// input buffer bounds | ||
let inputBegin := input.offset | ||
let inputEnd := add(inputBegin, input.length) | ||
|
||
// input traversal pointer | ||
let inputPtr := inputBegin | ||
// read length of the decompressed buffer | ||
let outputLength := 0 | ||
for {} lt(inputPtr, inputEnd) { | ||
inputPtr := add(inputPtr, 1) | ||
} { | ||
let c := byte(0, calldataload(inputPtr)) | ||
outputLength := add(outputLength, shl(mul(7, sub(inputPtr, inputBegin)), and(c, 0x7f))) | ||
if iszero(and(c, 0x80)) { | ||
break | ||
} | ||
} | ||
inputPtr := add(inputPtr, 1) | ||
|
||
// allocated output buffer | ||
output := mload(0x40) | ||
let outputPtr := add(output, 0x20) | ||
mstore(output, outputLength) | ||
mstore(0x40, add(outputPtr, outputLength)) | ||
|
||
// decompress input buffer into output buffer | ||
for { | ||
let len, offset | ||
} lt(inputPtr, inputEnd) {} { | ||
// get next (compressed) word -- used as a cache for further reads | ||
let w := calldataload(inputPtr) | ||
inputPtr := add(inputPtr, 1) | ||
|
||
let c := byte(0, w) | ||
|
||
// consider different cases based on the lower 2 bits of c | ||
// - 0: literal | ||
// - 1,2,3: offset copy | ||
switch and(c, 0x3) | ||
case 0 { | ||
len := add(shr(2, c), 1) | ||
if gt(len, 60) { | ||
assert(lt(add(inputPtr, 3), inputEnd), errorSelector) | ||
let smallLen := sub(len, 60) | ||
len := or(or(byte(1, w), shl(8, byte(2, w))), or(shl(16, byte(3, w)), shl(24, byte(4, w)))) | ||
len := add(and(len, shr(sub(256, mul(8, smallLen)), not(0))), 1) | ||
inputPtr := add(inputPtr, smallLen) | ||
} | ||
assert(not(gt(add(inputPtr, len), inputEnd)), errorSelector) | ||
// copy len bytes from input to output in chunks of 32 bytes | ||
calldatacopy(outputPtr, inputPtr, len) | ||
inputPtr := add(inputPtr, len) | ||
outputPtr := add(outputPtr, len) | ||
|
||
// continue to skip the offset copy logic that is shared by the other 3 cases | ||
continue | ||
} | ||
case 1 { | ||
assert(lt(inputPtr, inputEnd), errorSelector) | ||
len := add(and(shr(2, c), 0x7), 4) | ||
offset := add(byte(1, w), shl(8, shr(5, c))) | ||
inputPtr := add(inputPtr, 1) | ||
} | ||
case 2 { | ||
assert(lt(add(inputPtr, 1), inputEnd), errorSelector) | ||
len := add(shr(2, c), 1) | ||
offset := add(byte(1, w), shl(8, byte(2, w))) | ||
inputPtr := add(inputPtr, 2) | ||
} | ||
case 3 { | ||
assert(lt(add(inputPtr, 3), inputEnd), errorSelector) | ||
len := add(shr(2, c), 1) | ||
offset := add(add(byte(1, w), shl(8, byte(2, w))), add(shl(16, byte(3, w)), shl(24, byte(4, w)))) | ||
inputPtr := add(inputPtr, 4) | ||
} | ||
assert(and(iszero(iszero(offset)), not(gt(offset, sub(outputPtr, add(output, 0x20))))), errorSelector) | ||
|
||
// copying in chunks will not work if the offset is larger than the chunk length, so we compute | ||
// `step = Math.min(0x20, offset)` and use it for the memory copy | ||
let step := xor(0x20, mul(lt(offset, 0x20), xor(0x20, offset))) | ||
|
||
// copy len bytes from output to itself. | ||
for { | ||
let i := 0 | ||
} lt(i, len) { | ||
i := add(i, step) | ||
} { | ||
mstore(add(outputPtr, i), mload(sub(add(outputPtr, i), offset))) | ||
} | ||
outputPtr := add(outputPtr, len) | ||
} | ||
|
||
// sanity check, FMP is at the right location | ||
assert(eq(outputPtr, mload(0x40)), errorSelector) | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.