Skip to content

Commit

Permalink
Merge pull request #193 from matter-labs/bh-evm-321-enforce-correct-r…
Browse files Browse the repository at this point in the history
…elatively-optimal-compression-for-bytecodes

feat(sc): improved bytecode compression validity checks
  • Loading branch information
vladbochok committed Mar 5, 2024
2 parents 6da6bfa + 354001f commit 2118e77
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 21 deletions.
4 changes: 2 additions & 2 deletions system-contracts/SystemContractsHashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"contractName": "Compressor",
"bytecodePath": "artifacts-zk/contracts-preprocessed/Compressor.sol/Compressor.json",
"sourceCodePath": "contracts-preprocessed/Compressor.sol",
"bytecodeHash": "0x01000167edd0ca23181d430b5daf4966900b2fdad9bf555e4a8ae9314b648039",
"sourceCodeHash": "0x25ff4b50b5373f4fed1ae95f461a4547bb45bf5255ca94d8645b046aaab026a6"
"bytecodeHash": "0x0100016d4b23e09b564294711357cc6f0f85baa62f137d366c8b4f8f9bd0bc10",
"sourceCodeHash": "0xe0e22aa80843159daff6f09ed907c42d0d0d55225d04ba35b211389e05264f39"
},
{
"contractName": "ContractDeployer",
Expand Down
9 changes: 7 additions & 2 deletions system-contracts/contracts/Compressor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ contract Compressor is ICompressor, ISystemContract {
/// - 2 bytes: the length of the dictionary
/// - N bytes: the dictionary
/// - M bytes: the encoded data
/// @return bytecodeHash The hash of the original bytecode.
/// @dev The dictionary is a sequence of 8-byte chunks, each of them has the associated index.
/// @dev The encoded data is a sequence of 2-byte chunks, each of them is an index of the dictionary.
/// @dev The compression algorithm works as follows:
Expand All @@ -39,20 +40,24 @@ contract Compressor is ICompressor, ISystemContract {
/// * The 2-byte index of the chunk in the dictionary is added to the encoded data.
/// @dev Currently, the method may be called only from the bootloader because the server is not ready to publish bytecodes
/// in internal transactions. However, in the future, we will allow everyone to publish compressed bytecodes.
/// @dev Read more about the compression: https://github.com/matter-labs/zksync-era/blob/main/docs/guides/advanced/compression.md
function publishCompressedBytecode(
bytes calldata _bytecode,
bytes calldata _rawCompressedData
) external payable onlyCallFromBootloader returns (bytes32 bytecodeHash) {
unchecked {
(bytes calldata dictionary, bytes calldata encodedData) = _decodeRawBytecode(_rawCompressedData);

require(dictionary.length % 8 == 0, "Dictionary length should be a multiple of 8");
require(dictionary.length <= 2 ** 16 * 8, "Dictionary is too big");
require(
encodedData.length * 4 == _bytecode.length,
"Encoded data length should be 4 times shorter than the original bytecode"
);

require(
dictionary.length / 8 <= encodedData.length / 2,
"Dictionary should have at most the same number of entries as the encoded data"
);

for (uint256 encodedDataPointer = 0; encodedDataPointer < encodedData.length; encodedDataPointer += 2) {
uint256 indexOfEncodedChunk = uint256(encodedData.readUint16(encodedDataPointer)) * 8;
require(indexOfEncodedChunk < dictionary.length, "Encoded chunk index is out of bounds");
Expand Down
105 changes: 88 additions & 17 deletions system-contracts/test/Compressor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,54 +45,107 @@ describe("Compressor tests", function () {
});

describe("publishCompressedBytecode", function () {
it("non-bootloader failed to call", async () => {
it("should revert when it's a non-bootloader call", async () => {
await expect(compressor.publishCompressedBytecode("0x", "0x0000")).to.be.revertedWith(
"Callable only by the bootloader"
);
});

it("invalid encoded length", async () => {
const BYTECODE = "0xdeadbeefdeadbeef";
const COMPRESSED_BYTECODE = "0x0001deadbeefdeadbeef00000000";
it("should revert when the dictionary length is incorrect", async () => {
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
// Dictionary has only 1 entry, but the dictionary length is 2
const COMPRESSED_BYTECODE = "0x0002" + "deadbeefdeadbeef" + "0000" + "0000" + "0000" + "0000";
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("Encoded data length should be 4 times shorter than the original bytecode");
});

it("chunk index is out of bounds", async () => {
const BYTECODE = "0xdeadbeefdeadbeef";
const COMPRESSED_BYTECODE = "0x0001deadbeefdeadbeef0001";
it("should revert when there is no encoded data", async () => {
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
// Dictionary has 2 entries, but there is no encoded data
const COMPRESSED_BYTECODE = "0x0002" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("Encoded data length should be 4 times shorter than the original bytecode");
});

it("should revert when the encoded data length is invalid", async () => {
// Bytecode length is 32 bytes (4 chunks)
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
// Compressed bytecode is 14 bytes
// Dictionary length is 2 bytes
// Dictionary is 8 bytes (1 entry)
// Encoded data is 4 bytes
const COMPRESSED_BYTECODE = "0x0001" + "deadbeefdeadbeef" + "00000000";
// The length of the encodedData should be 32 / 4 = 8 bytes
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("Encoded data length should be 4 times shorter than the original bytecode");
});

it("should revert when the dictionary has too many entries", async () => {
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
// Dictionary has 5 entries
// Encoded data has 4 entries
const COMPRESSED_BYTECODE =
"0x0005" +
"deadbeefdeadbeef" +
"deadbeefdeadbeef" +
"deadbeefdeadbeef" +
"deadbeefdeadbeef" +
"deadbeefdeadbeef" +
"0000" +
"0000" +
"0000" +
"0000";
// The dictionary should have at most encode data length entries
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("Dictionary should have at most the same number of entries as the encoded data");
});

it("should revert when the encoded data has chunks where index is out of bounds", async () => {
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
// Dictionary has 1 entry
// Encoded data has 4 entries, three 0000 and one 0001
const COMPRESSED_BYTECODE = "0x0001" + "deadbeefdeadbeef" + "0000" + "0000" + "0000" + "0001";
// The dictionary has only 1 entry, so at the last entry of the encoded data the chunk index is out of bounds
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("Encoded chunk index is out of bounds");
});

it("chunk does not match the original bytecode", async () => {
const BYTECODE = "0xdeadbeefdeadbeef1111111111111111";
const COMPRESSED_BYTECODE = "0x0002deadbeefdeadbeef111111111111111100000000";
it("should revert when the encoded data has chunks that does not match the original bytecode", async () => {
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "1111111111111111";
// Encoded data has 4 entries, but the first one points to the wrong chunk of the dictionary
const COMPRESSED_BYTECODE =
"0x0002" + "deadbeefdeadbeef" + "1111111111111111" + "0001" + "0000" + "0000" + "0001";
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("Encoded chunk does not match the original bytecode");
});

it("invalid bytecode length in bytes", async () => {
const BYTECODE = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
const COMPRESSED_BYTECODE = "0x0001deadbeefdeadbeef000000000000";
it("should revert when the bytecode length in bytes is invalid", async () => {
// Bytecode length is 24 bytes (3 chunks), which is invalid because it's not a multiple of 32
const BYTECODE = "0x" + "deadbeefdeadbeef" + "deadbeefdeadbeef" + "deadbeefdeadbeef";
const COMPRESSED_BYTECODE = "0x0001" + "deadbeefdeadbeef" + "0000" + "0000" + "0000";
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("po");
});

// Test case with too big bytecode is unrealistic because API cannot accept so much data.
it("invalid bytecode length in words", async () => {
it("should revert when the bytecode length in words is odd", async () => {
// Bytecode length is 2 words (64 bytes), which is invalid because it's odd
const BYTECODE = "0x" + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".repeat(2);
const COMPRESSED_BYTECODE = "0x0001deadbeefdeadbeef" + "0000".repeat(4 * 2);
const COMPRESSED_BYTECODE = "0x0001" + "deadbeefdeadbeef" + "0000".repeat(4 * 2);
await expect(
compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE)
).to.be.revertedWith("pr");
});

it("successfully published", async () => {
// Test case with too big bytecode is unrealistic because API cannot accept so much data.

it("should successfully publish the bytecode", async () => {
const BYTECODE =
"0x000200000000000200010000000103550000006001100270000000150010019d0000000101200190000000080000c13d0000000001000019004e00160000040f0000000101000039004e00160000040f0000001504000041000000150510009c000000000104801900000040011002100000000001310019000000150320009c0000000002048019000000600220021000000000012100190000004f0001042e000000000100001900000050000104300000008002000039000000400020043f0000000002000416000000000110004c000000240000613d000000000120004c0000004d0000c13d000000200100003900000100001004430000012000000443000001000100003900000040020000390000001d03000041004e000a0000040f000000000120004c0000004d0000c13d0000000001000031000000030110008c0000004d0000a13d0000000101000367000000000101043b0000001601100197000000170110009c0000004d0000c13d0000000101000039000000000101041a0000000202000039000000000202041a000000400300043d00000040043000390000001805200197000000000600041a0000000000540435000000180110019700000020043000390000000000140435000000a0012002700000001901100197000000600430003900000000001404350000001a012001980000001b010000410000000001006019000000b8022002700000001c02200197000000000121019f0000008002300039000000000012043500000018016001970000000000130435000000400100043d0000000002130049000000a0022000390000000003000019004e000a0000040f004e00140000040f0000004e000004320000004f0001042e000000500001043000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffff000000000000000000000000000000000000000000000000000000008903573000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000ffffff0000000000008000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80000000000000000000000000000000000000000000000000000000000000007fffff00000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
const COMPRESSED_BYTECODE =
Expand All @@ -108,6 +161,24 @@ describe("Compressor tests", function () {
await encodeCalldata("KnownCodesStorage", "markBytecodeAsPublished", [zksync.utils.hashBytecode(BYTECODE)])
);
});

// documentation example from https://github.com/matter-labs/zksync-era/blob/main/docs/guides/advanced/compression.md
it("documentation example", async () => {
const BYTECODE =
"0x000000000000000A000000000000000D000000000000000A000000000000000C000000000000000B000000000000000A000000000000000D000000000000000A000000000000000D000000000000000A000000000000000B000000000000000B";
const COMPRESSED_BYTECODE =
"0x0004000000000000000A000000000000000D000000000000000B000000000000000C000000010000000300020000000100000001000000020002";
await setResult("L1Messenger", "sendToL1", [COMPRESSED_BYTECODE], {
failure: false,
returnData: ethers.constants.HashZero,
});
await expect(compressor.connect(bootloaderAccount).publishCompressedBytecode(BYTECODE, COMPRESSED_BYTECODE))
.to.emit(getMock("KnownCodesStorage"), "Called")
.withArgs(
0,
await encodeCalldata("KnownCodesStorage", "markBytecodeAsPublished", [zksync.utils.hashBytecode(BYTECODE)])
);
});
});

describe("verifyCompressedStateDiffs", function () {
Expand Down

0 comments on commit 2118e77

Please sign in to comment.