Skip to content
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

feat(sc): improved bytecode compression validity checks #193

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
vladbochok marked this conversation as resolved.
Show resolved Hide resolved
require(dictionary.length <= 2 ** 16 * 8, "Dictionary is too big");
vladbochok marked this conversation as resolved.
Show resolved Hide resolved
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
Loading